548 lines
23 KiB
Java
548 lines
23 KiB
Java
/*
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.cellbroadcastreceiver;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.content.ContentProvider;
|
|
import android.content.ContentProviderClient;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.UriMatcher;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteQueryBuilder;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import android.os.UserManager;
|
|
import android.provider.Telephony;
|
|
import android.telephony.SmsCbCmasInfo;
|
|
import android.telephony.SmsCbEtwsInfo;
|
|
import android.telephony.SmsCbLocation;
|
|
import android.telephony.SmsCbMessage;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
|
|
import java.util.concurrent.CountDownLatch;
|
|
|
|
/**
|
|
* ContentProvider for the database of received cell broadcasts.
|
|
*/
|
|
public class CellBroadcastContentProvider extends ContentProvider {
|
|
private static final String TAG = "CellBroadcastContentProvider";
|
|
|
|
/** URI matcher for ContentProvider queries. */
|
|
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
|
|
|
/** Authority string for content URIs. */
|
|
@VisibleForTesting
|
|
public static final String CB_AUTHORITY = "cellbroadcasts-app";
|
|
|
|
/** Content URI for notifying observers. */
|
|
static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts-app/");
|
|
|
|
/** URI matcher type to get all cell broadcasts. */
|
|
private static final int CB_ALL = 0;
|
|
|
|
/** URI matcher type to get a cell broadcast by ID. */
|
|
private static final int CB_ALL_ID = 1;
|
|
|
|
/** MIME type for the list of all cell broadcasts. */
|
|
private static final String CB_LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
|
|
|
|
/** MIME type for an individual cell broadcast. */
|
|
private static final String CB_TYPE = "vnd.android.cursor.item/cellbroadcast";
|
|
|
|
public static final String CALL_MIGRATION_METHOD = "migrate-legacy-data";
|
|
|
|
static {
|
|
sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL);
|
|
sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID);
|
|
}
|
|
|
|
/**
|
|
* The database for this content provider. Before using this we need to wait on
|
|
* mInitializedLatch, which counts down once initialization finishes in a background thread
|
|
*/
|
|
|
|
@VisibleForTesting
|
|
public CellBroadcastDatabaseHelper mOpenHelper;
|
|
|
|
// Latch which counts down from 1 when initialization in CellBroadcastOpenHelper.tryToMigrateV13
|
|
// is finished
|
|
private final CountDownLatch mInitializedLatch = new CountDownLatch(1);
|
|
|
|
/**
|
|
* Initialize content provider.
|
|
* @return true if the provider was successfully loaded, false otherwise
|
|
*/
|
|
@Override
|
|
public boolean onCreate() {
|
|
mOpenHelper = new CellBroadcastDatabaseHelper(getContext(), false);
|
|
// trigger this to create database explicitly. Otherwise the db will be created only after
|
|
// the first query/update/insertion. Data migration is done inside db creation and we want
|
|
// to migrate data from cellbroadcast-legacy immediately when upgrade to the mainline module
|
|
// rather than migrate after the first emergency alert.
|
|
// getReadable database will also call tryToMigrateV13 which copies the DB file to allow
|
|
// for safe rollbacks.
|
|
// This is done in a background thread to avoid triggering an ANR if the disk operations are
|
|
// too slow, and all other database uses should wait for the latch.
|
|
new Thread(() -> {
|
|
mOpenHelper.getReadableDatabase();
|
|
mInitializedLatch.countDown();
|
|
}).start();
|
|
return true;
|
|
}
|
|
|
|
protected SQLiteDatabase awaitInitAndGetWritableDatabase() {
|
|
while (mInitializedLatch.getCount() != 0) {
|
|
try {
|
|
mInitializedLatch.await();
|
|
} catch (InterruptedException e) {
|
|
Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e);
|
|
}
|
|
}
|
|
return mOpenHelper.getWritableDatabase();
|
|
}
|
|
|
|
protected SQLiteDatabase awaitInitAndGetReadableDatabase() {
|
|
while (mInitializedLatch.getCount() != 0) {
|
|
try {
|
|
mInitializedLatch.await();
|
|
} catch (InterruptedException e) {
|
|
Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e);
|
|
}
|
|
}
|
|
return mOpenHelper.getReadableDatabase();
|
|
}
|
|
|
|
/**
|
|
* Return a cursor for the cell broadcast table.
|
|
* @param uri the URI to query.
|
|
* @param projection the list of columns to put into the cursor, or null.
|
|
* @param selection the selection criteria to apply when filtering rows, or null.
|
|
* @param selectionArgs values to replace ?s in selection string.
|
|
* @param sortOrder how the rows in the cursor should be sorted, or null to sort from most
|
|
* recently received to least recently received.
|
|
* @return a Cursor or null.
|
|
*/
|
|
@Override
|
|
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
|
String sortOrder) {
|
|
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
|
qb.setTables(CellBroadcastDatabaseHelper.TABLE_NAME);
|
|
|
|
int match = sUriMatcher.match(uri);
|
|
switch (match) {
|
|
case CB_ALL:
|
|
// get all broadcasts
|
|
break;
|
|
|
|
case CB_ALL_ID:
|
|
// get broadcast by ID
|
|
qb.appendWhere("(_id=" + uri.getPathSegments().get(0) + ')');
|
|
break;
|
|
|
|
default:
|
|
Log.e(TAG, "Invalid query: " + uri);
|
|
throw new IllegalArgumentException("Unknown URI: " + uri);
|
|
}
|
|
|
|
String orderBy;
|
|
if (!TextUtils.isEmpty(sortOrder)) {
|
|
orderBy = sortOrder;
|
|
} else {
|
|
orderBy = Telephony.CellBroadcasts.DEFAULT_SORT_ORDER;
|
|
}
|
|
|
|
SQLiteDatabase db = awaitInitAndGetReadableDatabase();
|
|
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
|
|
if (c != null) {
|
|
c.setNotificationUri(getContext().getContentResolver(), CONTENT_URI);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Return the MIME type of the data at the specified URI.
|
|
* @param uri the URI to query.
|
|
* @return a MIME type string, or null if there is no type.
|
|
*/
|
|
@Override
|
|
public String getType(Uri uri) {
|
|
int match = sUriMatcher.match(uri);
|
|
switch (match) {
|
|
case CB_ALL:
|
|
return CB_LIST_TYPE;
|
|
|
|
case CB_ALL_ID:
|
|
return CB_TYPE;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a new row. This throws an exception, as the database can only be modified by
|
|
* calling custom methods in this class, and not via the ContentProvider interface.
|
|
* @param uri the content:// URI of the insertion request.
|
|
* @param values a set of column_name/value pairs to add to the database.
|
|
* @return the URI for the newly inserted item.
|
|
*/
|
|
@Override
|
|
public Uri insert(Uri uri, ContentValues values) {
|
|
throw new UnsupportedOperationException("insert not supported");
|
|
}
|
|
|
|
/**
|
|
* Delete one or more rows. This throws an exception, as the database can only be modified by
|
|
* calling custom methods in this class, and not via the ContentProvider interface.
|
|
* @param uri the full URI to query, including a row ID (if a specific record is requested).
|
|
* @param selection an optional restriction to apply to rows when deleting.
|
|
* @return the number of rows affected.
|
|
*/
|
|
@Override
|
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
throw new UnsupportedOperationException("delete not supported");
|
|
}
|
|
|
|
/**
|
|
* Update one or more rows. This throws an exception, as the database can only be modified by
|
|
* calling custom methods in this class, and not via the ContentProvider interface.
|
|
* @param uri the URI to query, potentially including the row ID.
|
|
* @param values a Bundle mapping from column names to new column values.
|
|
* @param selection an optional filter to match rows to update.
|
|
* @return the number of rows affected.
|
|
*/
|
|
@Override
|
|
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
throw new UnsupportedOperationException("update not supported");
|
|
}
|
|
|
|
@Override
|
|
public Bundle call(String method, String name, Bundle args) {
|
|
Log.d(TAG, "call:"
|
|
+ " method=" + method
|
|
+ " name=" + name
|
|
+ " args=" + args);
|
|
// this is to handle a content-provider defined method: migration
|
|
if (CALL_MIGRATION_METHOD.equals(method)) {
|
|
mOpenHelper.migrateFromLegacyIfNeeded(awaitInitAndGetReadableDatabase());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private ContentValues getContentValues(SmsCbMessage message) {
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(Telephony.CellBroadcasts.SLOT_INDEX, message.getSlotIndex());
|
|
cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, message.getGeographicalScope());
|
|
SmsCbLocation location = message.getLocation();
|
|
cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn());
|
|
if (location.getLac() != -1) {
|
|
cv.put(Telephony.CellBroadcasts.LAC, location.getLac());
|
|
}
|
|
if (location.getCid() != -1) {
|
|
cv.put(Telephony.CellBroadcasts.CID, location.getCid());
|
|
}
|
|
cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, message.getSerialNumber());
|
|
cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, message.getServiceCategory());
|
|
cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, message.getLanguageCode());
|
|
cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, message.getMessageBody());
|
|
cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, message.getReceivedTime());
|
|
cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, message.getMessageFormat());
|
|
cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, message.getMessagePriority());
|
|
|
|
SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
|
|
if (etwsInfo != null) {
|
|
cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType());
|
|
}
|
|
|
|
SmsCbCmasInfo cmasInfo = message.getCmasWarningInfo();
|
|
if (cmasInfo != null) {
|
|
cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass());
|
|
cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory());
|
|
cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType());
|
|
cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity());
|
|
cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency());
|
|
cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty());
|
|
}
|
|
|
|
return cv;
|
|
}
|
|
|
|
/**
|
|
* Internal method to insert a new Cell Broadcast into the database and notify observers.
|
|
* @param message the message to insert
|
|
* @return true if the broadcast is new, false if it's a duplicate broadcast.
|
|
*/
|
|
@VisibleForTesting
|
|
public boolean insertNewBroadcast(SmsCbMessage message) {
|
|
SQLiteDatabase db = awaitInitAndGetWritableDatabase();
|
|
ContentValues cv = getContentValues(message);
|
|
|
|
// Note: this method previously queried the database for duplicate message IDs, but this
|
|
// is not compatible with CMAS carrier requirements and could also cause other emergency
|
|
// alerts, e.g. ETWS, to not display if the database is filled with old messages.
|
|
// Use duplicate message ID detection in CellBroadcastAlertService instead of DB query.
|
|
long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv);
|
|
if (rowId == -1) {
|
|
Log.e(TAG, "failed to insert new broadcast into database");
|
|
// Return true on DB write failure because we still want to notify the user.
|
|
// The SmsCbMessage will be passed with the intent, so the message will be
|
|
// displayed in the emergency alert dialog, or the dialog that is displayed when
|
|
// the user selects the notification for a non-emergency broadcast, even if the
|
|
// broadcast could not be written to the database.
|
|
}
|
|
return true; // broadcast is not a duplicate
|
|
}
|
|
|
|
/**
|
|
* Internal method to delete a cell broadcast by row ID and notify observers.
|
|
* @param rowId the row ID of the broadcast to delete
|
|
* @return true if the database was updated, false otherwise
|
|
*/
|
|
@VisibleForTesting
|
|
public boolean deleteBroadcast(long rowId) {
|
|
SQLiteDatabase db = awaitInitAndGetWritableDatabase();
|
|
|
|
int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME,
|
|
Telephony.CellBroadcasts._ID + "=?",
|
|
new String[]{Long.toString(rowId)});
|
|
if (rowCount != 0) {
|
|
return true;
|
|
} else {
|
|
Log.e(TAG, "failed to delete broadcast at row " + rowId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to delete all cell broadcasts and notify observers.
|
|
* @return true if the database was updated, false otherwise
|
|
*/
|
|
@VisibleForTesting
|
|
public boolean deleteAllBroadcasts() {
|
|
SQLiteDatabase db = awaitInitAndGetWritableDatabase();
|
|
|
|
int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null);
|
|
if (rowCount != 0) {
|
|
return true;
|
|
} else {
|
|
Log.e(TAG, "failed to delete all broadcasts");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to mark a broadcast as read and notify observers. The broadcast can be
|
|
* identified by delivery time (for new alerts) or by row ID. The caller is responsible for
|
|
* decrementing the unread non-emergency alert count, if necessary.
|
|
*
|
|
* @param columnName the column name to query (ID or delivery time)
|
|
* @param columnValue the ID or delivery time of the broadcast to mark read
|
|
* @return true if the database was updated, false otherwise
|
|
*/
|
|
boolean markBroadcastRead(String columnName, long columnValue) {
|
|
SQLiteDatabase db = awaitInitAndGetWritableDatabase();
|
|
|
|
ContentValues cv = new ContentValues(1);
|
|
cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1);
|
|
|
|
String whereClause = columnName + "=?";
|
|
String[] whereArgs = new String[]{Long.toString(columnValue)};
|
|
|
|
int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs);
|
|
if (rowCount != 0) {
|
|
return true;
|
|
} else {
|
|
Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to mark a broadcast received in direct boot mode. After user unlocks, mark
|
|
* all messages not in direct boot mode.
|
|
*
|
|
* @param columnName the column name to query (ID or delivery time)
|
|
* @param columnValue the ID or delivery time of the broadcast to mark read
|
|
* @param isSmsSyncPending whether the message was pending for SMS inbox synchronization
|
|
* @return true if the database was updated, false otherwise
|
|
*/
|
|
@VisibleForTesting
|
|
public boolean markBroadcastSmsSyncPending(String columnName, long columnValue,
|
|
boolean isSmsSyncPending) {
|
|
SQLiteDatabase db = awaitInitAndGetWritableDatabase();
|
|
|
|
ContentValues cv = new ContentValues(1);
|
|
cv.put(CellBroadcastDatabaseHelper.SMS_SYNC_PENDING, isSmsSyncPending ? 1 : 0);
|
|
|
|
String whereClause = columnName + "=?";
|
|
String[] whereArgs = new String[]{Long.toString(columnValue)};
|
|
|
|
int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause,
|
|
whereArgs);
|
|
if (rowCount != 0) {
|
|
return true;
|
|
} else {
|
|
Log.e(TAG, "failed to mark broadcast pending for sms inbox sync: " + isSmsSyncPending
|
|
+ " where: " + columnName + " = " + columnValue);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write message to sms inbox if pending. e.g, when receive alerts in direct boot mode, we
|
|
* might need to sync message to sms inbox after user unlock.
|
|
* @param context
|
|
*/
|
|
|
|
@VisibleForTesting
|
|
public void resyncToSmsInbox(@NonNull Context context) {
|
|
// query all messages currently marked as sms inbox sync pending
|
|
try (Cursor cursor = query(
|
|
CellBroadcastContentProvider.CONTENT_URI,
|
|
CellBroadcastDatabaseHelper.QUERY_COLUMNS,
|
|
CellBroadcastDatabaseHelper.SMS_SYNC_PENDING + "=1",
|
|
null, null)) {
|
|
if (cursor != null) {
|
|
while (cursor.moveToNext()) {
|
|
SmsCbMessage message = CellBroadcastCursorAdapter
|
|
.createFromCursor(context, cursor);
|
|
if (message != null) {
|
|
Log.d(TAG, "handling message received pending for sms sync: "
|
|
+ message.toString());
|
|
writeMessageToSmsInbox(message, context);
|
|
// mark message received in direct mode was handled
|
|
markBroadcastSmsSyncPending(
|
|
Telephony.CellBroadcasts.DELIVERY_TIME,
|
|
message.getReceivedTime(), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write displayed cellbroadcast messages to sms inbox
|
|
*
|
|
* @param message The cell broadcast message.
|
|
*/
|
|
@VisibleForTesting
|
|
public void writeMessageToSmsInbox(@NonNull SmsCbMessage message, @NonNull Context context) {
|
|
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
|
|
if (!userManager.isSystemUser()) {
|
|
// SMS database is single-user mode, discard non-system users to avoid inserting twice.
|
|
Log.d(TAG, "ignoring writeMessageToSmsInbox due to non-system user");
|
|
return;
|
|
}
|
|
// Note SMS database is not direct boot aware for privacy reasons, we should only interact
|
|
// with sms db until users has unlocked.
|
|
if (!userManager.isUserUnlocked()) {
|
|
Log.d(TAG, "ignoring writeMessageToSmsInbox due to direct boot mode");
|
|
// need to retry after user unlock
|
|
markBroadcastSmsSyncPending(Telephony.CellBroadcasts.DELIVERY_TIME,
|
|
message.getReceivedTime(), true);
|
|
return;
|
|
}
|
|
// composing SMS
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(Telephony.Sms.Inbox.BODY, message.getMessageBody());
|
|
cv.put(Telephony.Sms.Inbox.DATE, message.getReceivedTime());
|
|
cv.put(Telephony.Sms.Inbox.SUBSCRIPTION_ID, message.getSubscriptionId());
|
|
cv.put(Telephony.Sms.Inbox.SUBJECT, context.getString(
|
|
CellBroadcastResources.getDialogTitleResource(context, message)));
|
|
cv.put(Telephony.Sms.Inbox.ADDRESS,
|
|
CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message));
|
|
cv.put(Telephony.Sms.Inbox.THREAD_ID, Telephony.Threads.getOrCreateThreadId(context,
|
|
CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message)));
|
|
if (CellBroadcastSettings.getResources(context, message.getSubscriptionId())
|
|
.getBoolean(R.bool.always_mark_sms_read)) {
|
|
// Always mark SMS message READ. End users expect when they read new CBS messages,
|
|
// the unread alert count in the notification should be decreased, as they thought it
|
|
// was coming from SMS. Now we are marking those SMS as read (SMS now serve as a message
|
|
// history purpose) and that should give clear messages to end-users that alerts are not
|
|
// from the SMS app but CellBroadcast and they should tap the notification to read alert
|
|
// in order to see decreased unread message count.
|
|
cv.put(Telephony.Sms.Inbox.READ, 1);
|
|
}
|
|
Uri uri = context.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, cv);
|
|
if (uri == null) {
|
|
Log.e(TAG, "writeMessageToSmsInbox: failed");
|
|
} else {
|
|
Log.d(TAG, "writeMessageToSmsInbox: succeed uri = " + uri);
|
|
}
|
|
}
|
|
|
|
/** Callback for users of AsyncCellBroadcastOperation. */
|
|
interface CellBroadcastOperation {
|
|
/**
|
|
* Perform an operation using the specified provider.
|
|
* @param provider the CellBroadcastContentProvider to use
|
|
* @return true if any rows were changed, false otherwise
|
|
*/
|
|
boolean execute(CellBroadcastContentProvider provider);
|
|
}
|
|
|
|
/**
|
|
* Async task to call this content provider's internal methods on a background thread.
|
|
* The caller supplies the CellBroadcastOperation object to call for this provider.
|
|
*/
|
|
static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> {
|
|
/** Reference to this app's content resolver. */
|
|
private ContentResolver mContentResolver;
|
|
|
|
AsyncCellBroadcastTask(ContentResolver contentResolver) {
|
|
mContentResolver = contentResolver;
|
|
}
|
|
|
|
/**
|
|
* Perform a generic operation on the CellBroadcastContentProvider.
|
|
* @param params the CellBroadcastOperation object to call for this provider
|
|
* @return void
|
|
*/
|
|
@Override
|
|
protected Void doInBackground(CellBroadcastOperation... params) {
|
|
ContentProviderClient cpc = mContentResolver.acquireContentProviderClient(
|
|
CellBroadcastContentProvider.CB_AUTHORITY);
|
|
CellBroadcastContentProvider provider = (CellBroadcastContentProvider)
|
|
cpc.getLocalContentProvider();
|
|
|
|
if (provider != null) {
|
|
try {
|
|
boolean changed = params[0].execute(provider);
|
|
if (changed) {
|
|
Log.d(TAG, "database changed: notifying observers...");
|
|
mContentResolver.notifyChange(CONTENT_URI, null, false);
|
|
}
|
|
} finally {
|
|
cpc.release();
|
|
}
|
|
} else {
|
|
Log.e(TAG, "getLocalContentProvider() returned null");
|
|
}
|
|
|
|
mContentResolver = null; // free reference to content resolver
|
|
return null;
|
|
}
|
|
}
|
|
}
|