diff options
Diffstat (limited to 'src/com/android/providers')
15 files changed, 619 insertions, 237 deletions
diff --git a/src/com/android/providers/contacts/AbstractContactsProvider.java b/src/com/android/providers/contacts/AbstractContactsProvider.java index ebf0a1b9..e00bd862 100644 --- a/src/com/android/providers/contacts/AbstractContactsProvider.java +++ b/src/com/android/providers/contacts/AbstractContactsProvider.java @@ -16,10 +16,6 @@ package com.android.providers.contacts; -import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.Tables; - import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; @@ -38,8 +34,11 @@ import android.provider.BaseColumns; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.util.Log; -import android.util.SparseBooleanArray; -import android.util.SparseLongArray; + +import com.android.internal.util.ProviderAccessStats; +import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; +import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; +import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import java.io.PrintWriter; import java.util.ArrayList; @@ -116,23 +115,8 @@ public abstract class AbstractContactsProvider extends ContentProvider */ private SQLiteTransactionListener mSerializedDbTransactionListener; - private final long mStartTime = SystemClock.elapsedRealtime(); - - private final Object mStatsLock = new Object(); - protected final SparseBooleanArray mAllCallingUids = new SparseBooleanArray(); - protected final SparseLongArray mQueryStats = new SparseLongArray(); - protected final SparseLongArray mBatchStats = new SparseLongArray(); - protected final SparseLongArray mInsertStats = new SparseLongArray(); - protected final SparseLongArray mUpdateStats = new SparseLongArray(); - protected final SparseLongArray mDeleteStats = new SparseLongArray(); - protected final SparseLongArray mInsertInBatchStats = new SparseLongArray(); - protected final SparseLongArray mUpdateInBatchStats = new SparseLongArray(); - protected final SparseLongArray mDeleteInBatchStats = new SparseLongArray(); - private final SparseLongArray mOperationDurationMicroStats = new SparseLongArray(); - - private final ThreadLocal<Integer> mOperationNest = ThreadLocal.withInitial(() -> 0); - private final ThreadLocal<Long> mOperationStartNs = ThreadLocal.withInitial(() -> 0L); + protected final ProviderAccessStats mStats = new ProviderAccessStats(); @Override public boolean onCreate() { @@ -158,47 +142,19 @@ public abstract class AbstractContactsProvider extends ContentProvider mSerializedDbTransactionListener = listener; } - protected final void incrementStats(SparseLongArray stats) { - final int callingUid = Binder.getCallingUid(); - synchronized (mStatsLock) { - stats.put(callingUid, stats.get(callingUid) + 1); - mAllCallingUids.put(callingUid, true); - - final int nest = mOperationNest.get(); - mOperationNest.set(nest + 1); - if (nest == 0) { - mOperationStartNs.set(SystemClock.elapsedRealtimeNanos()); - } - } + public ContactsTransaction getCurrentTransaction() { + return mTransactionHolder.get(); } - protected final void incrementStats(SparseLongArray statsNonBatch, - SparseLongArray statsInBatch) { + private boolean isInBatch() { final ContactsTransaction t = mTransactionHolder.get(); - final boolean inBatch = t != null && t.isBatch(); - incrementStats(inBatch ? statsInBatch : statsNonBatch); - } - - protected void finishOperation() { - final int callingUid = Binder.getCallingUid(); - synchronized (mStatsLock) { - final int nest = mOperationNest.get(); - mOperationNest.set(nest - 1); - if (nest == 1) { - final long duration = SystemClock.elapsedRealtimeNanos() - mOperationStartNs.get(); - mOperationDurationMicroStats.put(callingUid, - mOperationDurationMicroStats.get(callingUid) + duration / 1000L); - } - } - } - - public ContactsTransaction getCurrentTransaction() { - return mTransactionHolder.get(); + return t != null && t.isBatch(); } @Override public Uri insert(Uri uri, ContentValues values) { - incrementStats(mInsertStats, mInsertInBatchStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementInsertStats(callingUid, isInBatch()); try { ContactsTransaction transaction = startTransaction(false); try { @@ -212,13 +168,14 @@ public abstract class AbstractContactsProvider extends ContentProvider endTransaction(false); } } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - incrementStats(mDeleteStats, mDeleteInBatchStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementDeleteStats(callingUid, isInBatch()); try { ContactsTransaction transaction = startTransaction(false); try { @@ -232,13 +189,14 @@ public abstract class AbstractContactsProvider extends ContentProvider endTransaction(false); } } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - incrementStats(mUpdateStats, mUpdateInBatchStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementUpdateStats(callingUid, isInBatch()); try { ContactsTransaction transaction = startTransaction(false); try { @@ -252,13 +210,14 @@ public abstract class AbstractContactsProvider extends ContentProvider endTransaction(false); } } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } @Override public int bulkInsert(Uri uri, ContentValues[] values) { - incrementStats(mBatchStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementBatchStats(callingUid); try { ContactsTransaction transaction = startTransaction(true); int numValues = values.length; @@ -282,14 +241,15 @@ public abstract class AbstractContactsProvider extends ContentProvider } return numValues; } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } @Override public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { - incrementStats(mBatchStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementBatchStats(callingUid); try { if (VERBOSE_LOGGING) { Log.v(TAG, "applyBatch: " + operations.size() + " ops"); @@ -331,7 +291,7 @@ public abstract class AbstractContactsProvider extends ContentProvider endTransaction(true); } } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } @@ -462,32 +422,7 @@ public abstract class AbstractContactsProvider extends ContentProvider pw.print("Database: "); pw.println(dbName); - pw.print(" Uptime: "); - pw.print((SystemClock.elapsedRealtime() - mStartTime) / (60 * 1000)); - pw.println(" minutes"); - - synchronized (mStatsLock) { - pw.println(); - pw.println(" Client activities:"); - pw.println(" UID Query Insert Update Delete Batch Insert Update Delete" - + " Sec"); - for (int i = 0; i < mAllCallingUids.size(); i++) { - final int uid = mAllCallingUids.keyAt(i); - pw.println(String.format( - " %-9d %6d %6d %6d %6d %6d %6d %6d %6d %12.3f", - uid, - mQueryStats.get(uid), - mInsertStats.get(uid), - mUpdateStats.get(uid), - mDeleteStats.get(uid), - mBatchStats.get(uid), - mInsertInBatchStats.get(uid), - mUpdateInBatchStats.get(uid), - mDeleteInBatchStats.get(uid), - (mOperationDurationMicroStats.get(uid) / 1000000.0) - )); - } - } + mStats.dump(pw, " "); if (mDbHelper == null) { pw.println("mDbHelper is null"); diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java index 59e9b147..f71a7503 100644 --- a/src/com/android/providers/contacts/CallLogProvider.java +++ b/src/com/android/providers/contacts/CallLogProvider.java @@ -22,10 +22,13 @@ import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClau import android.app.AppOpsManager; import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; @@ -45,11 +48,15 @@ import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ProviderAccessStats; import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties; import com.android.providers.contacts.CallLogDatabaseHelper.Tables; import com.android.providers.contacts.util.SelectionBuilder; import com.android.providers.contacts.util.UserUtils; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -175,6 +182,10 @@ public class CallLogProvider extends ContentProvider { private VoicemailPermissions mVoicemailPermissions; private CallLogInsertionHelper mCallLogInsertionHelper; + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>(); + private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>(); + private final ProviderAccessStats mStats = new ProviderAccessStats(); + protected boolean isShadow() { return false; } @@ -228,9 +239,58 @@ public class CallLogProvider extends ContentProvider { return CallLogDatabaseHelper.getInstance(context); } + protected boolean applyingBatch() { + final Boolean applying = mApplyingBatch.get(); + return applying != null && applying; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + final int callingUid = Binder.getCallingUid(); + mCallingUid.set(callingUid); + + mStats.incrementBatchStats(callingUid); + mApplyingBatch.set(true); + try { + return super.applyBatch(operations); + } finally { + mApplyingBatch.set(false); + mStats.finishOperation(callingUid); + } + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + final int callingUid = Binder.getCallingUid(); + mCallingUid.set(callingUid); + + mStats.incrementBatchStats(callingUid); + mApplyingBatch.set(true); + try { + return super.bulkInsert(uri, values); + } finally { + mApplyingBatch.set(false); + mStats.finishOperation(callingUid); + } + } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + // Note don't use mCallingUid here. That's only used by mutation functions. + final int callingUid = Binder.getCallingUid(); + + mStats.incrementQueryStats(callingUid); + try { + return queryInternal(uri, projection, selection, selectionArgs, sortOrder); + } finally { + mStats.finishOperation(callingUid); + } + } + + private Cursor queryInternal(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { if (VERBOSE_LOGGING) { Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + @@ -361,6 +421,44 @@ public class CallLogProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { + final int callingUid = + applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); + + mStats.incrementInsertStats(callingUid, applyingBatch()); + try { + return insertInternal(uri, values); + } finally { + mStats.finishOperation(callingUid); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + final int callingUid = + applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); + + mStats.incrementInsertStats(callingUid, applyingBatch()); + try { + return updateInternal(uri, values, selection, selectionArgs); + } finally { + mStats.finishOperation(callingUid); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + final int callingUid = + applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); + + mStats.incrementInsertStats(callingUid, applyingBatch()); + try { + return deleteInternal(uri, selection, selectionArgs); + } finally { + mStats.finishOperation(callingUid); + } + } + + private Uri insertInternal(Uri uri, ContentValues values) { if (VERBOSE_LOGGING) { Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" + " CPID=" + Binder.getCallingPid()); @@ -383,15 +481,15 @@ public class CallLogProvider extends ContentProvider { // Add the computed fields to the copied values. mCallLogInsertionHelper.addComputedValues(copiedValues); - long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues); + long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues); if (rowId > 0) { return ContentUris.withAppendedId(uri, rowId); } return null; } - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + private int updateInternal(Uri uri, ContentValues values, + String selection, String[] selectionArgs) { if (VERBOSE_LOGGING) { Log.v(TAG, "update: uri=" + uri + " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + @@ -423,12 +521,11 @@ public class CallLogProvider extends ContentProvider { throw new UnsupportedOperationException("Cannot update URL: " + uri); } - return getDatabaseModifier(db).update(uri, Tables.CALLS, values, selectionBuilder.build(), + return createDatabaseModifier(db).update(uri, Tables.CALLS, values, selectionBuilder.build(), selectionArgs); } - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { + private int deleteInternal(Uri uri, String selection, String[] selectionArgs) { if (VERBOSE_LOGGING) { Log.v(TAG, "delete: uri=" + uri + " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + @@ -445,7 +542,7 @@ public class CallLogProvider extends ContentProvider { case CALLS: // TODO: Special case - We may want to forward the delete request on user 0 to the // shadow provider too. - return getDatabaseModifier(db).delete(Tables.CALLS, + return createDatabaseModifier(db).delete(Tables.CALLS, selectionBuilder.build(), selectionArgs); default: throw new UnsupportedOperationException("Cannot delete that URL: " + uri); @@ -460,15 +557,15 @@ public class CallLogProvider extends ContentProvider { * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications * after the operation is performed. */ - private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { + private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { return new DbModifierWithNotification(Tables.CALLS, db, getContext()); } /** - * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations + * Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations * only. */ - private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { + private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext()); } @@ -753,4 +850,9 @@ public class CallLogProvider extends ContentProvider { public void shutdown() { mTaskScheduler.shutdownForTest(); } + + @Override + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mStats.dump(writer, " "); + } } diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java index 76fe173d..9414ece8 100644 --- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java +++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java @@ -136,9 +136,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { * 1000-1099 M * 1100-1199 N * 1200-1299 O + * 1300-1399 P * </pre> */ - static final int DATABASE_VERSION = 1202; + static final int DATABASE_VERSION = 1300; private static final int MINIMUM_SUPPORTED_VERSION = 700; @VisibleForTesting @@ -1440,7 +1441,9 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { Data.SYNC2 + " TEXT, " + Data.SYNC3 + " TEXT, " + Data.SYNC4 + " TEXT, " + - Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0 " + + Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0, " + + Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + " TEXT, " + + Data.PREFERRED_PHONE_ACCOUNT_ID + " TEXT " + ");"); db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" + @@ -1924,6 +1927,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + Data.DATA14 + ", " + Data.DATA15 + ", " + Data.CARRIER_PRESENCE + ", " + + Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + ", " + + Data.PREFERRED_PHONE_ACCOUNT_ID + ", " + Data.SYNC1 + ", " + Data.SYNC2 + ", " + Data.SYNC3 + ", " @@ -2666,6 +2671,12 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { oldVersion = 1202; } + if (isUpgradeRequired(oldVersion,newVersion, 1300)) { + upgradeToVersion1300(db); + upgradeViewsAndTriggers = true; + oldVersion = 1300; + } + // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here // yet, until CallLogDatabaseHelper moves the data. @@ -3438,6 +3449,15 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + "last_time_used = 0"); } + public void upgradeToVersion1300(SQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE data ADD preferred_phone_account_component_name " + + "TEXT;"); + db.execSQL("ALTER TABLE data ADD preferred_phone_account_id TEXT;"); + } catch (SQLiteException ignore) { + } + } + /** * This method is only used in upgradeToVersion1101 method, and should not be used in other * places now. Because data15 is not used to generate hash_id for photo, and the new generating diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index a5af51fe..54b4a27d 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -754,6 +754,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .add(Data.DATA14) .add(Data.DATA15) .add(Data.CARRIER_PRESENCE) + .add(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME) + .add(Data.PREFERRED_PHONE_ACCOUNT_ID) .add(Data.DATA_VERSION) .add(Data.IS_PRIMARY) .add(Data.IS_SUPER_PRIMARY) @@ -4649,6 +4651,9 @@ public class ContactsProvider2 extends AbstractContactsProvider if (flagExists(values, RawContacts.STARRED)) { if (!callerIsSyncAdapter) { updateFavoritesMembership(rawContactId, flagIsSet(values, RawContacts.STARRED)); + mTransactionContext.get().markRawContactDirtyAndChanged( + rawContactId, callerIsSyncAdapter); + mSyncToNetwork |= !callerIsSyncAdapter; } aggregator.updateStarred(rawContactId); aggregator.updatePinned(rawContactId); @@ -4662,6 +4667,9 @@ public class ContactsProvider2 extends AbstractContactsProvider SELECTION_STARRED_FROM_RAW_CONTACTS, new String[] {Long.toString(rawContactId)}); updateFavoritesMembership(rawContactId, starred); + mTransactionContext.get().markRawContactDirtyAndChanged( + rawContactId, callerIsSyncAdapter); + mSyncToNetwork |= !callerIsSyncAdapter; } } if (flagExists(values, RawContacts.SEND_TO_VOICEMAIL)) { @@ -4832,6 +4840,7 @@ public class ContactsProvider2 extends AbstractContactsProvider if (hasStarredValue) { updateFavoritesMembership(rawContactId, flagIsSet(values, RawContacts.STARRED)); + mSyncToNetwork |= !callerIsSyncAdapter; } if (hasStarredValue || hasPinnedValue || hasVoiceMailValue) { @@ -5536,7 +5545,8 @@ public class ContactsProvider2 extends AbstractContactsProvider return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); } - incrementStats(mQueryStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementQueryStats(callingUid); try { // Otherwise proceed with a normal query against the contacts DB. switchToContactMode(); @@ -5544,7 +5554,7 @@ public class ContactsProvider2 extends AbstractContactsProvider return queryDirectoryIfNecessary(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } diff --git a/src/com/android/providers/contacts/DatabaseModifier.java b/src/com/android/providers/contacts/DatabaseModifier.java index b11605b4..60f9c7f1 100644 --- a/src/com/android/providers/contacts/DatabaseModifier.java +++ b/src/com/android/providers/contacts/DatabaseModifier.java @@ -49,4 +49,10 @@ public interface DatabaseModifier { * {@link SQLiteDatabase#delete(String, String, String[])} method. */ public abstract int delete(String table, String whereClause, String[] whereArgs); + + void startBulkOperation(); + + void yieldBulkOperation(); + + void finishBulkOperation(); } diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java index 7e7b3e17..852301d3 100644 --- a/src/com/android/providers/contacts/DbModifierWithNotification.java +++ b/src/com/android/providers/contacts/DbModifierWithNotification.java @@ -17,7 +17,6 @@ package com.android.providers.contacts; -import static android.Manifest.permission.ADD_VOICEMAIL; import static android.Manifest.permission.READ_VOICEMAIL; import android.content.ComponentName; @@ -25,8 +24,6 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ResolveInfo; import android.database.Cursor; import android.database.DatabaseUtils.InsertHelper; import android.database.sqlite.SQLiteDatabase; @@ -37,17 +34,15 @@ import android.provider.VoicemailContract; import android.provider.VoicemailContract.Status; import android.provider.VoicemailContract.Voicemails; import android.util.ArraySet; -import android.util.Log; import com.android.common.io.MoreCloseables; +import com.android.internal.annotations.VisibleForTesting; import com.android.providers.contacts.CallLogDatabaseHelper.Tables; import com.android.providers.contacts.util.DbQueryUtils; import com.google.android.collect.Lists; import com.google.common.collect.Iterables; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Set; /** @@ -57,6 +52,7 @@ import java.util.Set; * of then got affected by the change. */ public class DbModifierWithNotification implements DatabaseModifier { + private static final String TAG = "DbModifierWithNotify"; private static final String[] PROJECTION = new String[] { @@ -73,8 +69,11 @@ public class DbModifierWithNotification implements DatabaseModifier { private final Context mContext; private final Uri mBaseUri; private final boolean mIsCallsTable; - private final VoicemailPermissions mVoicemailPermissions; + private final VoicemailNotifier mVoicemailNotifier; + + private boolean mIsBulkOperation = false; + private static VoicemailNotifier sVoicemailNotifierForTest; public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) { this(tableName, db, null, context); @@ -94,7 +93,8 @@ public class DbModifierWithNotification implements DatabaseModifier { mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ? Status.CONTENT_URI : Voicemails.CONTENT_URI; mIsCallsTable = mTableName.equals(Tables.CALLS); - mVoicemailPermissions = new VoicemailPermissions(mContext); + mVoicemailNotifier = sVoicemailNotifierForTest != null ? sVoicemailNotifierForTest + : new VoicemailNotifier(mContext, mBaseUri); } @Override @@ -143,13 +143,21 @@ public class DbModifierWithNotification implements DatabaseModifier { } } - private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) { + private void notifyVoicemailChangeOnInsert( + Uri notificationUri, Set<String> packagesModified) { if (mIsCallsTable) { - notifyVoicemailChange(notificationUri, packagesModified, - VoicemailContract.ACTION_NEW_VOICEMAIL, Intent.ACTION_PROVIDER_CHANGED); - } else { - notifyVoicemailChange(notificationUri, packagesModified, - Intent.ACTION_PROVIDER_CHANGED); + mVoicemailNotifier.addIntentActions(VoicemailContract.ACTION_NEW_VOICEMAIL); + } + notifyVoicemailChange(notificationUri, packagesModified); + } + + private void notifyVoicemailChange(Uri notificationUri, + Set<String> modifiedPackages) { + mVoicemailNotifier.addUri(notificationUri); + mVoicemailNotifier.addModifiedPackages(modifiedPackages); + mVoicemailNotifier.addIntentActions(Intent.ACTION_PROVIDER_CHANGED); + if (!mIsBulkOperation) { + mVoicemailNotifier.sendNotification(); } } @@ -159,7 +167,8 @@ public class DbModifierWithNotification implements DatabaseModifier { Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs); packagesModified.addAll(getModifiedPackages(values)); - boolean isVoicemail = packagesModified.size() != 0; + boolean isVoicemailContent = + packagesModified.size() != 0 && isUpdatingVoicemailColumns(values); boolean hasMarkedRead = false; if (mIsCallsTable) { @@ -169,35 +178,27 @@ public class DbModifierWithNotification implements DatabaseModifier { } else { updateLastModified(table, whereClause, whereArgs); } - if (isVoicemail) { - // If a calling package is modifying its own entries, it means that the change came - // from the server and thus is synced or "clean". Otherwise, it means that a local - // change is being made to the database, so the entries should be marked as "dirty" - // so that the corresponding sync adapter knows they need to be synced. - int isDirty; - Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY); - if (callerSetDirty != null) { - // Respect the calling package if it sets the dirty flag - isDirty = callerSetDirty == 0 ? 0 : 1; - } else { - isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1; - } - values.put(VoicemailContract.Voicemails.DIRTY, isDirty); - - if (isDirty == 0 && values.containsKey(Calls.IS_READ) && getAsBoolean(values, - Calls.IS_READ)) { - // If the server has set the IS_READ, it should also unset the new flag - if (!values.containsKey(Calls.NEW)) { - values.put(Calls.NEW, 0); - hasMarkedRead = true; + if (isVoicemailContent) { + if (updateDirtyFlag(values, packagesModified)) { + if (values.containsKey(Calls.IS_READ) + && getAsBoolean(values, + Calls.IS_READ)) { + // If the server has set the IS_READ, it should also unset the new flag + if (!values.containsKey(Calls.NEW)) { + values.put(Calls.NEW, 0); + hasMarkedRead = true; + } } } } } - + // updateDirtyFlag might remove the value and leave values empty. + if (values.isEmpty()) { + return 0; + } int count = mDb.update(table, values, whereClause, whereArgs); - if (count > 0 && isVoicemail) { - notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED); + if (count > 0 && isVoicemailContent || Tables.VOICEMAIL_STATUS.equals(table)) { + notifyVoicemailChange(mBaseUri, packagesModified); } if (count > 0 && mIsCallsTable) { notifyCallLogChange(); @@ -213,6 +214,38 @@ public class DbModifierWithNotification implements DatabaseModifier { return count; } + private boolean updateDirtyFlag(ContentValues values, Set<String> packagesModified) { + // If a calling package is modifying its own entries, it means that the change came + // from the server and thus is synced or "clean". Otherwise, it means that a local + // change is being made to the database, so the entries should be marked as "dirty" + // so that the corresponding sync adapter knows they need to be synced. + int isDirty; + Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY); + if (callerSetDirty != null) { + // Respect the calling package if it sets the dirty flag + if (callerSetDirty == Voicemails.DIRTY_RETAIN) { + values.remove(Voicemails.DIRTY); + return false; + } else { + isDirty = callerSetDirty == 0 ? 0 : 1; + } + } else { + isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1; + } + + values.put(Voicemails.DIRTY, isDirty); + return isDirty == 0; + } + + private boolean isUpdatingVoicemailColumns(ContentValues values) { + for (String key : values.keySet()) { + if (VoicemailContentTable.ALLOWED_COLUMNS.contains(key)) { + return true; + } + } + return false; + } + private void updateLastModified(String table, String whereClause, String[] whereArgs) { ContentValues values = new ContentValues(); values.put(Calls.LAST_MODIFIED, getTimeMillis()); @@ -247,7 +280,7 @@ public class DbModifierWithNotification implements DatabaseModifier { } if (count > 0 && isVoicemail) { - notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED); + notifyVoicemailChange(mBaseUri, packagesModified); } if (count > 0 && mIsCallsTable) { notifyCallLogChange(); @@ -255,6 +288,25 @@ public class DbModifierWithNotification implements DatabaseModifier { return count; } + @Override + public void startBulkOperation() { + mIsBulkOperation = true; + mDb.beginTransaction(); + } + + @Override + public void yieldBulkOperation() { + mDb.yieldIfContendedSafely(); + } + + @Override + public void finishBulkOperation() { + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + mIsBulkOperation = false; + mVoicemailNotifier.sendNotification(); + } + /** * Returns the set of packages affected when a modify operation is run for the specified * where clause. When called from an insert operation an empty set returned by this method @@ -266,7 +318,7 @@ public class DbModifierWithNotification implements DatabaseModifier { Cursor cursor = mDb.query(mTableName, PROJECTION, DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause), whereArgs, null, null, null); - while(cursor.moveToNext()) { + while (cursor.moveToNext()) { modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX)); } MoreCloseables.closeQuietly(cursor); @@ -281,7 +333,7 @@ public class DbModifierWithNotification implements DatabaseModifier { */ private Set<String> getModifiedPackages(ContentValues values) { Set<String> impactedPackages = new ArraySet<>(); - if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { + if (values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD)); } return impactedPackages; @@ -290,7 +342,7 @@ public class DbModifierWithNotification implements DatabaseModifier { /** * @param packagesModified source packages that inserted the voicemail that is being modified * @return {@code true} if the caller is modifying its own voicemail, or this is an internal - * transaction, {@code false} otherwise. + * transaction, {@code false} otherwise. */ private boolean isSelfModifyingOrInternal(Set<String> packagesModified) { final Collection<String> callingPackages = getCallingPackages(); @@ -301,59 +353,7 @@ public class DbModifierWithNotification implements DatabaseModifier { // but allows us to mock the results for testing. return packagesModified.size() == 1 && (callingPackages.contains( Iterables.getOnlyElement(packagesModified)) - || callingPackages.contains(mContext.getPackageName())); - } - - private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages, - String... intentActions) { - // Notify the observers. - // Must be done only once, even if there are multiple broadcast intents. - mContext.getContentResolver().notifyChange(notificationUri, null, true); - Collection<String> callingPackages = getCallingPackages(); - // Now fire individual intents. - for (String intentAction : intentActions) { - // self_change extra should be included only for provider_changed events. - boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED); - for (ComponentName component : - getBroadcastReceiverComponents(intentAction, notificationUri)) { - // Ignore any package that is not affected by the change and don't have full access - // either. - if (!modifiedPackages.contains(component.getPackageName()) && - !mVoicemailPermissions.packageHasReadAccess( - component.getPackageName())) { - continue; - } - - Intent intent = new Intent(intentAction, notificationUri); - intent.setComponent(component); - if (includeSelfChangeExtra && callingPackages != null) { - intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE, - callingPackages.contains(component.getPackageName())); - } - String permissionNeeded = modifiedPackages.contains(component.getPackageName()) ? - ADD_VOICEMAIL : READ_VOICEMAIL; - mContext.sendBroadcast(intent, permissionNeeded); - Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s, perm:%s," + - " self_change:%s", intent.getAction(), intent.getData(), - component.getClassName(), permissionNeeded, - intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ? - intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) : - null)); - } - } - } - - /** Determines the components that can possibly receive the specified intent. */ - private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) { - Intent intent = new Intent(intentAction, uri); - List<ComponentName> receiverComponents = new ArrayList<ComponentName>(); - // For broadcast receivers ResolveInfo.activityInfo is the one that is populated. - for (ResolveInfo resolveInfo : - mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) { - ActivityInfo activityInfo = resolveInfo.activityInfo; - receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name)); - } - return receiverComponents; + || callingPackages.contains(mContext.getPackageName())); } /** @@ -393,4 +393,9 @@ public class DbModifierWithNotification implements DatabaseModifier { } return CallLogProvider.getTimeForTestMillis(); } + + @VisibleForTesting + static void setVoicemailNotifierForTest(VoicemailNotifier notifier) { + sVoicemailNotifierForTest = notifier; + } } diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java index 6c84e4b0..f3b6daf6 100644 --- a/src/com/android/providers/contacts/ProfileProvider.java +++ b/src/com/android/providers/contacts/ProfileProvider.java @@ -24,6 +24,7 @@ import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.os.Binder; import android.os.CancellationSignal; import android.provider.ContactsContract.Intents; @@ -70,12 +71,13 @@ public class ProfileProvider extends AbstractContactsProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { - incrementStats(mQueryStats); + final int callingUid = Binder.getCallingUid(); + mStats.incrementQueryStats(callingUid); try { return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1, cancellationSignal); } finally { - finishOperation(); + mStats.finishOperation(callingUid); } } diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java index 14c78a77..e421654c 100644 --- a/src/com/android/providers/contacts/SearchIndexManager.java +++ b/src/com/android/providers/contacts/SearchIndexManager.java @@ -24,7 +24,6 @@ import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.ProviderStatus; import android.provider.ContactsContract.RawContacts; import android.text.TextUtils; import android.util.ArraySet; @@ -35,6 +34,8 @@ import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; +import com.android.providers.contacts.util.CappedStringBuilder; + import com.google.android.collect.Lists; import com.google.common.annotations.VisibleForTesting; @@ -51,6 +52,8 @@ public class SearchIndexManager { private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); + private static final int MAX_STRING_BUILDER_SIZE = 1024 * 10; + public static final String PROPERTY_SEARCH_INDEX_VERSION = "search_index"; private static final int SEARCH_INDEX_VERSION = 1; @@ -72,10 +75,11 @@ public class SearchIndexManager { public static final int SEPARATOR_SLASH = 2; public static final int SEPARATOR_COMMA = 3; - private StringBuilder mSbContent = new StringBuilder(); - private StringBuilder mSbName = new StringBuilder(); - private StringBuilder mSbTokens = new StringBuilder(); - private StringBuilder mSbElementContent = new StringBuilder(); + private CappedStringBuilder mSbContent = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE); + private CappedStringBuilder mSbName = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE); + private CappedStringBuilder mSbTokens = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE); + private CappedStringBuilder mSbElementContent = new CappedStringBuilder( + MAX_STRING_BUILDER_SIZE); private ArraySet<String> mUniqueElements = new ArraySet<>(); private Cursor mCursor; @@ -84,10 +88,10 @@ public class SearchIndexManager { } void reset() { - mSbContent.setLength(0); - mSbTokens.setLength(0); - mSbName.setLength(0); - mSbElementContent.setLength(0); + mSbContent.clear(); + mSbTokens.clear(); + mSbName.clear(); + mSbElementContent.clear(); mUniqueElements.clear(); } @@ -126,7 +130,7 @@ public class SearchIndexManager { mSbContent.append(content); mUniqueElements.add(content); } - mSbElementContent.setLength(0); + mSbElementContent.clear(); } } diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java index 160a1a99..1ced1be6 100644 --- a/src/com/android/providers/contacts/VoicemailContentProvider.java +++ b/src/com/android/providers/contacts/VoicemailContentProvider.java @@ -15,20 +15,22 @@ */ package com.android.providers.contacts; +import static android.app.AppOpsManager.MODE_ALLOWED; import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD; import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; +import android.annotation.NonNull; import android.app.AppOpsManager; import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Binder; +import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.provider.VoicemailContract; @@ -49,7 +51,6 @@ import com.google.common.annotations.VisibleForTesting; import java.io.FileNotFoundException; import java.util.Arrays; import java.util.List; -import java.util.Set; /** * An implementation of the Voicemail content provider. This class in the entry point for both @@ -82,8 +83,15 @@ public class VoicemailContentProvider extends ContentProvider } Context context = context(); - // ADD_VOICEMAIL permission guards read and write. We do the same with app ops. - // The permission name doesn't reflect its function but we cannot rename it. + // Read and write permission requires ADD_VOICEMAIL or carrier privileges. We can't declare + // any permission entries in the manifest because carrier-privileged apps without + // ADD_VOICEMAIL would be blocked by the platform without even reaching our custom + // enforce{Read,Write}PermissionInner functions. These overrides are what allow carrier- + // privileged apps to bypass these runtime-configured permissions. + // TODO(b/74245334): See if these can be removed since individual operations perform their + // own checks. + setReadPermission(android.Manifest.permission.ADD_VOICEMAIL); + setWritePermission(android.Manifest.permission.ADD_VOICEMAIL); setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL); mVoicemailPermissions = new VoicemailPermissions(context); @@ -109,6 +117,27 @@ public class VoicemailContentProvider extends ContentProvider return true; } + @Override + protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) + throws SecurityException { + // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state. + if (mVoicemailPermissions.callerHasCarrierPrivileges()) { + return MODE_ALLOWED; + } + return super.enforceReadPermissionInner(uri, callingPkg, callerToken); + } + + + @Override + protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken) + throws SecurityException { + // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state. + if (mVoicemailPermissions.callerHasCarrierPrivileges()) { + return MODE_ALLOWED; + } + return super.enforceWritePermissionInner(uri, callingPkg, callerToken); + } + @VisibleForTesting void scheduleScanStalePackages() { scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null); @@ -157,6 +186,12 @@ public class VoicemailContentProvider extends ContentProvider } @Override + public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { + UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values); + return getTableDelegate(uriData).bulkInsert(uriData, values); + } + + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (VERBOSE_LOGGING) { diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java index 09a8c1f0..67dda926 100644 --- a/src/com/android/providers/contacts/VoicemailContentTable.java +++ b/src/com/android/providers/contacts/VoicemailContentTable.java @@ -39,7 +39,6 @@ import com.android.providers.contacts.VoicemailContentProvider.UriData; import com.android.providers.contacts.util.CloseUtils; import com.google.common.collect.ImmutableSet; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -52,16 +51,19 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { private static final String TAG = "VmContentProvider"; private final ProjectionMap mVoicemailProjectionMap; - /** The private directory in which to store the data associated with the voicemail. */ + /** + * The private directory in which to store the data associated with the voicemail. + */ private static final String DATA_DIRECTORY = "voicemail-data"; - private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA }; + private static final String[] FILENAME_ONLY_PROJECTION = new String[] {Voicemails._DATA}; - private static final ImmutableSet<String> ALLOWED_COLUMNS = new ImmutableSet.Builder<String>() + public static final ImmutableSet<String> ALLOWED_COLUMNS = new ImmutableSet.Builder<String>() .add(Voicemails._ID) .add(Voicemails.NUMBER) .add(Voicemails.DATE) .add(Voicemails.DURATION) + .add(Voicemails.NEW) .add(Voicemails.IS_READ) .add(Voicemails.TRANSCRIPTION) .add(Voicemails.TRANSCRIPTION_STATE) @@ -83,6 +85,8 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { .add(OpenableColumns.SIZE) .build(); + private static final int BULK_INSERTS_PER_YIELD_POINT = 50; + private final String mTableName; private final CallLogDatabaseHelper mDbHelper; private final Context mContext; @@ -101,6 +105,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { .add(Voicemails.NUMBER) .add(Voicemails.DATE) .add(Voicemails.DURATION) + .add(Voicemails.NEW) .add(Voicemails.IS_READ) .add(Voicemails.TRANSCRIPTION) .add(Voicemails.TRANSCRIPTION_STATE) @@ -138,6 +143,30 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { @Override public Uri insert(UriData uriData, ContentValues values) { + DatabaseModifier modifier = createDatabaseModifier(mDbHelper.getWritableDatabase()); + Uri uri = insertRow(modifier, uriData, values); + return uri; + } + + @Override + public int bulkInsert(UriData uriData, ContentValues[] values) { + DatabaseModifier modifier = createDatabaseModifier(mDbHelper.getWritableDatabase()); + modifier.startBulkOperation(); + int count = 0; + for (ContentValues value : values) { + Uri uri = insertRow(modifier, uriData, value); + if (uri != null) { + count++; + } + if((count % BULK_INSERTS_PER_YIELD_POINT) == 0){ + modifier.yieldBulkOperation(); + } + } + modifier.finishBulkOperation(); + return count; + } + + private Uri insertRow(DatabaseModifier modifier, UriData uriData, ContentValues values) { checkForSupportedColumns(mVoicemailProjectionMap, values); ContentValues copiedValues = new ContentValues(values); checkInsertSupported(uriData); @@ -160,7 +189,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { } SQLiteDatabase db = mDbHelper.getWritableDatabase(); - long rowId = getDatabaseModifier(db).insert(mTableName, null, copiedValues); + long rowId = modifier.insert(mTableName, null, copiedValues); if (rowId > 0) { Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId); // Populate the 'voicemail_uri' field to be used by the call_log provider. @@ -228,7 +257,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { } // Now delete the rows themselves. - return getDatabaseModifier(db).delete(mTableName, combinedClause, + return createDatabaseModifier(db).delete(mTableName, combinedClause, selectionArgs); } @@ -262,7 +291,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { // URI that include message Id. I think we do want to support bulk update. String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), getCallTypeClause()); - return getDatabaseModifier(db).update(uriData.getUri(), mTableName, values, combinedClause, + return createDatabaseModifier(db).update(uriData.getUri(), mTableName, values, combinedClause, selectionArgs); } @@ -298,7 +327,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate { return getEqualityClause(Calls.TYPE, Calls.VOICEMAIL_TYPE); } - private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { + private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { return new DbModifierWithNotification(mTableName, db, mContext); } diff --git a/src/com/android/providers/contacts/VoicemailNotifier.java b/src/com/android/providers/contacts/VoicemailNotifier.java new file mode 100644 index 00000000..159cec73 --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailNotifier.java @@ -0,0 +1,128 @@ +package com.android.providers.contacts; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Binder; +import android.provider.VoicemailContract; +import android.util.ArraySet; +import android.util.Log; + +import com.google.android.collect.Lists; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Aggregates voicemail broadcasts from multiple operations in to a single one. The URIs will be + * {@link VoicemailContract.Voicemails#DIR_TYPE} instead of {@link + * VoicemailContract.Voicemails#ITEM_TYPE} if multiple URIs is notified. + */ +public class VoicemailNotifier { + + private final String TAG = "VoicemailNotifier"; + + private final Context mContext; + private final Uri mBaseUri; + + private final VoicemailPermissions mVoicemailPermissions; + + private final Set<String> mIntentActions = new ArraySet<>(); + private final Set<String> mModifiedPackages = new ArraySet<>(); + private final Set<Uri> mUris = new ArraySet<>(); + + public VoicemailNotifier(Context context, Uri baseUri) { + mContext = context; + mBaseUri = baseUri; + mVoicemailPermissions = new VoicemailPermissions(mContext); + } + + public void addIntentActions(String action) { + mIntentActions.add(action); + } + + public void addModifiedPackages(Collection<String> packages) { + mModifiedPackages.addAll(packages); + } + + public void addUri(Uri uri) { + mUris.add(uri); + } + + public void sendNotification() { + Uri uri = mUris.size() == 1 ? mUris.iterator().next() : mBaseUri; + mContext.getContentResolver().notifyChange(uri, null, true); + Collection<String> callingPackages = getCallingPackages(); + // Now fire individual intents. + for (String intentAction : mIntentActions) { + // self_change extra should be included only for provider_changed events. + boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED); + Log.i(TAG, "receivers for " + intentAction + " :" + getBroadcastReceiverComponents( + intentAction, uri)); + for (ComponentName component : + getBroadcastReceiverComponents(intentAction, uri)) { + boolean hasFullReadAccess = + mVoicemailPermissions.packageHasReadAccess(component.getPackageName()); + boolean hasOwnAccess = + mVoicemailPermissions.packageHasOwnVoicemailAccess( + component.getPackageName()); + // If we don't have full access, ignore the broadcast if the package isn't affected + // by the change or doesn't have access to its own messages. + if (!hasFullReadAccess + && (!mModifiedPackages.contains(component.getPackageName()) + || !hasOwnAccess)) { + continue; + } + + Intent intent = new Intent(intentAction, uri); + intent.setComponent(component); + if (includeSelfChangeExtra && callingPackages != null) { + intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE, + callingPackages.contains(component.getPackageName())); + } + mContext.sendBroadcast(intent); + Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s," + + " self_change:%s", intent.getAction(), intent.getData(), + component.getClassName(), + intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ? + intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) : + null)); + } + } + mIntentActions.clear(); + mModifiedPackages.clear(); + mUris.clear(); + } + + /** + * Returns the package names of the calling process. If the calling process has more than + * one packages, this returns them all + */ + private Collection<String> getCallingPackages() { + int caller = Binder.getCallingUid(); + if (caller == 0) { + return null; + } + return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller)); + } + + /** + * Determines the components that can possibly receive the specified intent. + */ + private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) { + Intent intent = new Intent(intentAction, uri); + List<ComponentName> receiverComponents = new ArrayList<ComponentName>(); + // For broadcast receivers ResolveInfo.activityInfo is the one that is populated. + for (ResolveInfo resolveInfo : + mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) { + ActivityInfo activityInfo = resolveInfo.activityInfo; + receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name)); + } + return receiverComponents; + } +} diff --git a/src/com/android/providers/contacts/VoicemailPermissions.java b/src/com/android/providers/contacts/VoicemailPermissions.java index 50f2447a..ed3815dc 100644 --- a/src/com/android/providers/contacts/VoicemailPermissions.java +++ b/src/com/android/providers/contacts/VoicemailPermissions.java @@ -16,10 +16,12 @@ package com.android.providers.contacts; -import com.android.providers.contacts.util.ContactsPermissions; - import android.content.Context; +import android.os.Binder; import android.telecom.DefaultDialerManager; +import android.telephony.TelephonyManager; + +import com.android.providers.contacts.util.ContactsPermissions; /** * Provides method related to check various voicemail permissions under the @@ -35,7 +37,8 @@ public class VoicemailPermissions { /** Determines if the calling process has access to its own voicemails. */ public boolean callerHasOwnVoicemailAccess() { - return callerHasPermission(android.Manifest.permission.ADD_VOICEMAIL); + return callerHasPermission(android.Manifest.permission.ADD_VOICEMAIL) + || callerHasCarrierPrivileges(); } /** Determine if the calling process has full read access to all voicemails. */ @@ -63,7 +66,7 @@ public class VoicemailPermissions { public void checkCallerHasOwnVoicemailAccess() { if (!callerHasOwnVoicemailAccess()) { throw new SecurityException("The caller must have permission: " + - android.Manifest.permission.ADD_VOICEMAIL); + android.Manifest.permission.ADD_VOICEMAIL + " or carrier privileges"); } } @@ -91,7 +94,8 @@ public class VoicemailPermissions { /** Determines if the given package has access to its own voicemails. */ public boolean packageHasOwnVoicemailAccess(String packageName) { return packageHasPermission(packageName, - android.Manifest.permission.ADD_VOICEMAIL); + android.Manifest.permission.ADD_VOICEMAIL) + || packageHasCarrierPrivileges(packageName); } /** Determines if the given package has read access. */ @@ -113,4 +117,25 @@ public class VoicemailPermissions { private boolean callerHasPermission(String permission) { return ContactsPermissions.hasCallerOrSelfPermission(mContext, permission); } + + /** Determines if the calling process has carrier privileges. */ + public boolean callerHasCarrierPrivileges() { + TelephonyManager tm = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + String[] packages = mContext.getPackageManager().getPackagesForUid(Binder.getCallingUid()); + for (String packageName : packages) { + if (tm.checkCarrierPrivilegesForPackageAnyPhone(packageName) + == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { + return true; + } + } + return false; + } + + /** Determines if the given package has carrier privileges. */ + private boolean packageHasCarrierPrivileges(String packageName) { + TelephonyManager tm = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + return tm.getPackagesWithCarrierPrivileges().contains(packageName); + } } diff --git a/src/com/android/providers/contacts/VoicemailStatusTable.java b/src/com/android/providers/contacts/VoicemailStatusTable.java index f3008c0e..5b03c8a6 100644 --- a/src/com/android/providers/contacts/VoicemailStatusTable.java +++ b/src/com/android/providers/contacts/VoicemailStatusTable.java @@ -32,8 +32,6 @@ import android.util.ArraySet; import com.android.common.content.ProjectionMap; import com.android.providers.contacts.VoicemailContentProvider.UriData; -import java.util.Set; - /** * Implementation of {@link VoicemailTable.Delegate} for the voicemail status table. * @@ -78,7 +76,7 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate { SQLiteDatabase db = mDbHelper.getWritableDatabase(); // Try to update before insert. String combinedClause = uriData.getWhereClause(); - int rowsChanged = getDatabaseModifier(db) + int rowsChanged = createDatabaseModifier(db) .update(uriData.getUri(), mTableName, values, combinedClause, null); if (rowsChanged != 0) { final String[] selection = new String[] {Status._ID}; @@ -90,7 +88,7 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate { } ContentValues copiedValues = new ContentValues(values); mDelegateHelper.checkAndAddSourcePackageIntoValues(uriData, copiedValues); - long rowId = getDatabaseModifier(db).insert(mTableName, null, copiedValues); + long rowId = createDatabaseModifier(db).insert(mTableName, null, copiedValues); if (rowId > 0) { return ContentUris.withAppendedId(uriData.getUri(), rowId); } else { @@ -100,11 +98,23 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate { } @Override + public int bulkInsert(UriData uriData, ContentValues[] values) { + int count = 0; + for (ContentValues value : values) { + Uri uri = insert(uriData, value); + if (uri != null) { + count++; + } + } + return count; + } + + @Override public int delete(UriData uriData, String selection, String[] selectionArgs) { synchronized (DATABASE_LOCK) { SQLiteDatabase db = mDbHelper.getWritableDatabase(); String combinedClause = concatenateClauses(selection, uriData.getWhereClause()); - return getDatabaseModifier(db).delete(mTableName, combinedClause, + return createDatabaseModifier(db).delete(mTableName, combinedClause, selectionArgs); } } @@ -135,7 +145,7 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate { synchronized (DATABASE_LOCK) { SQLiteDatabase db = mDbHelper.getWritableDatabase(); String combinedClause = concatenateClauses(selection, uriData.getWhereClause()); - return getDatabaseModifier(db) + return createDatabaseModifier(db) .update(uriData.getUri(), mTableName, values, combinedClause, selectionArgs); } } @@ -154,7 +164,7 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate { throw new UnsupportedOperationException("File operation is not supported for status table"); } - private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { + private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { return new DbModifierWithNotification(mTableName, db, mContext); } diff --git a/src/com/android/providers/contacts/VoicemailTable.java b/src/com/android/providers/contacts/VoicemailTable.java index fcb653ce..f71b50de 100644 --- a/src/com/android/providers/contacts/VoicemailTable.java +++ b/src/com/android/providers/contacts/VoicemailTable.java @@ -46,6 +46,8 @@ public interface VoicemailTable { public ParcelFileDescriptor openFile(UriData uriData, String mode) throws FileNotFoundException; public ArraySet<String> getSourcePackages(); + + int bulkInsert(UriData uriData, ContentValues[] values); } /** diff --git a/src/com/android/providers/contacts/util/CappedStringBuilder.java b/src/com/android/providers/contacts/util/CappedStringBuilder.java new file mode 100644 index 00000000..74b70cf5 --- /dev/null +++ b/src/com/android/providers/contacts/util/CappedStringBuilder.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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.providers.contacts.util; + +import android.util.Log; + +import com.android.providers.contacts.AbstractContactsProvider; + +public class CappedStringBuilder { + private final int mCapSize; + private boolean mOver; + private final StringBuilder mStringBuilder = new StringBuilder(); + + public CappedStringBuilder(int capSize) { + mCapSize = capSize; + } + + public void clear() { + mOver = false; + mStringBuilder.setLength(0); + } + + public int length() { + return mStringBuilder.length(); + } + + @Override + public String toString() { + return mStringBuilder.toString(); + } + + public CappedStringBuilder append(char ch) { + if (canAppend(mStringBuilder.length() + 1)) { + mStringBuilder.append(ch); + } + return this; + } + + public CappedStringBuilder append(String s) { + if (canAppend(mStringBuilder.length() + s.length())) { + mStringBuilder.append(s); + } + return this; + } + + private boolean canAppend(int length) { + if (mOver || length > mCapSize) { + if (!mOver && AbstractContactsProvider.VERBOSE_LOGGING) { + Log.w(AbstractContactsProvider.TAG, "String too long! new length=" + length); + } + mOver = true; + return false; + } + return true; + } +} |