diff options
Diffstat (limited to 'src/com')
28 files changed, 1304 insertions, 1173 deletions
diff --git a/src/com/android/providers/contacts/AccountWithDataSet.java b/src/com/android/providers/contacts/AccountWithDataSet.java index bfae1128..e1f633ea 100644 --- a/src/com/android/providers/contacts/AccountWithDataSet.java +++ b/src/com/android/providers/contacts/AccountWithDataSet.java @@ -17,10 +17,14 @@ package com.android.providers.contacts; import android.accounts.Account; +import android.provider.ContactsContract; +import android.provider.ContactsContract.SimAccount; import android.text.TextUtils; import com.google.common.base.Objects; +import java.util.List; + /** * Account information that includes the data set, if any. */ @@ -105,4 +109,23 @@ public class AccountWithDataSet { } return false; } + + /** + * @return {@code true} if there is a {@link SimAccount} with a matching account name and type + * in the passed list. + * The list should be obtained from {@link ContactsDatabaseHelper#getAllSimAccounts()} so it + * will already have valid SIM accounts so only name and type need to be compared. + */ + public boolean inSimAccounts(List<SimAccount> simAccountList) { + // Note we don't want to create a new SimAccount object from this instance, as this method + // does not need to know about the SIM slot or the EF type. It only needs to know whether + // the name and type match since the passed list will only contain valid SIM accounts. + for (SimAccount simAccount : simAccountList) { + if (Objects.equal(simAccount.getAccountName(), getAccountName()) + && Objects.equal(simAccount.getAccountType(), getAccountType())) { + return true; + } + } + return false; + } } diff --git a/src/com/android/providers/contacts/CallComposerLocationProvider.java b/src/com/android/providers/contacts/CallComposerLocationProvider.java new file mode 100644 index 00000000..568a1899 --- /dev/null +++ b/src/com/android/providers/contacts/CallComposerLocationProvider.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2021 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; + +import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentProvider; +import android.content.ContentUris; +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.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Binder; +import android.os.Process; +import android.provider.CallLog; +import android.telecom.TelecomManager; +import android.text.TextUtils; +import android.util.Log; + + +import com.android.providers.contacts.util.SelectionBuilder; + +import java.util.Objects; + +public class CallComposerLocationProvider extends ContentProvider { + private static final String TAG = CallComposerLocationProvider.class.getSimpleName(); + private static final String DB_NAME = "call_composer_locations.db"; + private static final String TABLE_NAME = "locations"; + private static final int VERSION = 1; + + private static final int LOCATION = 1; + private static final int LOCATION_ID = 2; + + private static class OpenHelper extends SQLiteOpenHelper { + public OpenHelper(@Nullable Context context, @Nullable String name, + @Nullable SQLiteDatabase.CursorFactory factory, int version) { + super(context, name, factory, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_NAME+ " (" + + CallLog.Locations._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + CallLog.Locations.LATITUDE + " REAL, " + + CallLog.Locations.LONGITUDE + " REAL" + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Nothing to do here, still on version 1. + } + } + + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + static { + sURIMatcher.addURI(CallLog.Locations.AUTHORITY, "", LOCATION); + sURIMatcher.addURI(CallLog.Locations.AUTHORITY, "/#", LOCATION_ID); + } + + private OpenHelper mOpenHelper; + + @Override + public boolean onCreate() { + mOpenHelper = new OpenHelper(getContext(), DB_NAME, null, VERSION); + return true; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, + @Nullable String[] selectionArgs, @Nullable String sortOrder) { + enforceAccessRestrictions(); + final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(TABLE_NAME); + qb.setStrict(true); + qb.setStrictGrammar(true); + final int match = sURIMatcher.match(uri); + + final SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + switch (match) { + case LOCATION_ID: { + selectionBuilder.addClause(getEqualityClause(CallLog.Locations._ID, + parseLocationIdFromUri(uri))); + break; + } + default: + throw new IllegalArgumentException("Provided URI is not supported for query."); + } + + final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + return qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, + null, sortOrder, null); + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + final int match = sURIMatcher.match(uri); + switch (match) { + case LOCATION_ID: + return CallLog.Locations.CONTENT_ITEM_TYPE; + case LOCATION: + return CallLog.Locations.CONTENT_TYPE; + default: + return null; + } + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + enforceAccessRestrictions(); + long id = mOpenHelper.getWritableDatabase().insert(TABLE_NAME, null, values); + return ContentUris.withAppendedId(CallLog.Locations.CONTENT_URI, id); + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, + @Nullable String[] selectionArgs) { + enforceAccessRestrictions(); + final int match = sURIMatcher.match(uri); + switch (match) { + case LOCATION_ID: + long id = parseLocationIdFromUri(uri); + return mOpenHelper.getWritableDatabase().delete(TABLE_NAME, + CallLog.Locations._ID + " = ?", new String[] {String.valueOf(id)}); + case LOCATION: + Log.w(TAG, "Deleting entire location table!"); + return mOpenHelper.getWritableDatabase().delete(TABLE_NAME, "1", null); + default: + throw new IllegalArgumentException("delete() on the locations" + + " does not support the uri " + uri.toString()); + } + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, + @Nullable String[] selectionArgs) { + enforceAccessRestrictions(); + throw new UnsupportedOperationException( + "Call composer location db does not support updates"); + } + + private long parseLocationIdFromUri(Uri uri) { + try { + return Long.parseLong(uri.getPathSegments().get(0)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid location id in uri: " + uri, e); + } + } + + private void enforceAccessRestrictions() { + int uid = Binder.getCallingUid(); + if (uid == Process.SYSTEM_UID || uid == Process.myUid() || uid == Process.PHONE_UID) { + return; + } + String defaultDialerPackageName = getContext().getSystemService(TelecomManager.class) + .getDefaultDialerPackage(); + if (TextUtils.isEmpty(defaultDialerPackageName)) { + throw new SecurityException("Access to call composer locations is only allowed for the" + + " default dialer, but the default dialer is unset"); + } + String[] callingPackageCandidates = getContext().getPackageManager().getPackagesForUid(uid); + for (String packageCandidate : callingPackageCandidates) { + if (Objects.equals(packageCandidate, defaultDialerPackageName)) { + return; + } + } + throw new SecurityException("Access to call composer locations is only allowed for the " + + "default dialer: " + defaultDialerPackageName); + } +} diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java index 06d3291b..22f1cad4 100644 --- a/src/com/android/providers/contacts/CallLogDatabaseHelper.java +++ b/src/com/android/providers/contacts/CallLogDatabaseHelper.java @@ -39,7 +39,7 @@ import com.android.providers.contacts.util.PropertyUtils; public class CallLogDatabaseHelper { private static final String TAG = "CallLogDatabaseHelper"; - private static final int DATABASE_VERSION = 9; + private static final int DATABASE_VERSION = 10; private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE @@ -153,6 +153,10 @@ public class CallLogDatabaseHelper { Calls.CALL_SCREENING_APP_NAME + " TEXT," + Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," + Calls.MISSED_REASON + " INTEGER NOT NULL DEFAULT 0," + + Calls.PRIORITY + " INTEGER NOT NULL DEFAULT 0," + + Calls.SUBJECT + " TEXT," + + Calls.LOCATION + " TEXT," + + Calls.COMPOSER_PHOTO_URI + " TEXT," + Voicemails._DATA + " TEXT," + Voicemails.HAS_CONTENT + " INTEGER," + @@ -225,6 +229,10 @@ public class CallLogDatabaseHelper { if (oldVersion < 9) { upgradeToVersion9(db); } + + if (oldVersion < 10) { + upgradeToVersion10(db); + } } } @@ -459,6 +467,12 @@ public class CallLogDatabaseHelper { db.execSQL("ALTER TABLE calls ADD missed_reason INTEGER NOT NULL DEFAULT 0"); } + private void upgradeToVersion10(SQLiteDatabase db) { + db.execSQL("ALTER TABLE calls ADD priority INTEGER NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE calls ADD subject TEXT"); + db.execSQL("ALTER TABLE calls ADD location TEXT"); + db.execSQL("ALTER TABLE calls ADD composer_photo_uri TEXT"); + } /** * Perform the migration from the contacts2.db (of the latest version) to the current calllog/ * voicemail status tables. diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java index 9fa651cb..c832e9b4 100644 --- a/src/com/android/providers/contacts/CallLogProvider.java +++ b/src/com/android/providers/contacts/CallLogProvider.java @@ -20,6 +20,8 @@ import static com.android.providers.contacts.util.DbQueryUtils.checkForSupported import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.ContentProvider; import android.content.ContentProviderOperation; @@ -36,6 +38,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; +import android.os.StatFs; import android.os.UserHandle; import android.os.UserManager; import android.provider.CallLog; @@ -43,6 +49,7 @@ import android.provider.CallLog.Calls; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -55,11 +62,25 @@ import com.android.providers.contacts.util.SelectionBuilder; import com.android.providers.contacts.util.UserUtils; import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; /** * Call log content provider. @@ -81,18 +102,57 @@ public class CallLogProvider extends ContentProvider { private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause( Calls.PHONE_ACCOUNT_HIDDEN, 0); + private static final String CALL_COMPOSER_PICTURE_DIRECTORY_NAME = "call_composer_pics"; + private static final String CALL_COMPOSER_ALL_USERS_DIRECTORY_NAME = "all_users"; + + // Constants to be used with ContentProvider#call in order to sync call composer pics between + // users. Defined here because they're for internal use only. + /** + * Method name used to get a list of {@link Uri}s for call composer pictures inserted for all + * users after a certain date + */ + private static final String GET_CALL_COMPOSER_IMAGE_URIS = + "com.android.providers.contacts.GET_CALL_COMPOSER_IMAGE_URIS"; + + /** + * Long-valued extra containing the date to filter by expressed as milliseconds after the epoch. + */ + private static final String EXTRA_SINCE_DATE = + "com.android.providers.contacts.extras.SINCE_DATE"; + + /** + * Boolean-valued extra indicating whether to read from the shadow portion of the calllog + * (i.e. device-encrypted storage rather than credential-encrypted) + */ + private static final String EXTRA_IS_SHADOW = + "com.android.providers.contacts.extras.IS_SHADOW"; + + /** + * Boolean-valued extra indicating whether to return Uris only for those images that are + * supposed to be inserted for all users. + */ + private static final String EXTRA_ALL_USERS_ONLY = + "com.android.providers.contacts.extras.ALL_USERS_ONLY"; + + private static final String EXTRA_RESULT_URIS = + "com.android.provider.contacts.extras.EXTRA_RESULT_URIS"; + @VisibleForTesting static final String[] CALL_LOG_SYNC_PROJECTION = new String[] { - Calls.NUMBER, - Calls.NUMBER_PRESENTATION, - Calls.TYPE, - Calls.FEATURES, - Calls.DATE, - Calls.DURATION, - Calls.DATA_USAGE, - Calls.PHONE_ACCOUNT_COMPONENT_NAME, - Calls.PHONE_ACCOUNT_ID, - Calls.ADD_FOR_ALL_USERS + Calls.NUMBER, + Calls.NUMBER_PRESENTATION, + Calls.TYPE, + Calls.FEATURES, + Calls.DATE, + Calls.DURATION, + Calls.DATA_USAGE, + Calls.PHONE_ACCOUNT_COMPONENT_NAME, + Calls.PHONE_ACCOUNT_ID, + Calls.PRIORITY, + Calls.SUBJECT, + Calls.COMPOSER_PHOTO_URI, + // Location is deliberately omitted + Calls.ADD_FOR_ALL_USERS }; static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID }; @@ -103,6 +163,10 @@ public class CallLogProvider extends ContentProvider { private static final int CALLS_FILTER = 3; + private static final int CALL_COMPOSER_NEW_PICTURE = 4; + + private static final int CALL_COMPOSER_PICTURE = 5; + private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY = "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " + Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;"; @@ -116,12 +180,20 @@ public class CallLogProvider extends ContentProvider { sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER); + sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, + CALL_COMPOSER_NEW_PICTURE); + sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + "/*", + CALL_COMPOSER_PICTURE); - // Shadow provider only supports "/calls". + // Shadow provider only supports "/calls" and "/call_composer". sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS); + sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, + CALL_COMPOSER_NEW_PICTURE); + sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + "/*", + CALL_COMPOSER_PICTURE); } - private static final ArrayMap<String, String> sCallsProjectionMap; + public static final ArrayMap<String, String> sCallsProjectionMap; static { // Calls projection map @@ -163,6 +235,10 @@ public class CallLogProvider extends ContentProvider { sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME); sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON); sCallsProjectionMap.put(Calls.MISSED_REASON, Calls.MISSED_REASON); + sCallsProjectionMap.put(Calls.PRIORITY, Calls.PRIORITY); + sCallsProjectionMap.put(Calls.COMPOSER_PHOTO_URI, Calls.COMPOSER_PHOTO_URI); + sCallsProjectionMap.put(Calls.SUBJECT, Calls.SUBJECT); + sCallsProjectionMap.put(Calls.LOCATION, Calls.LOCATION); } private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts"; @@ -443,6 +519,11 @@ public class CallLogProvider extends ContentProvider { return Calls.CONTENT_ITEM_TYPE; case CALLS_FILTER: return Calls.CONTENT_TYPE; + case CALL_COMPOSER_NEW_PICTURE: + return null; // No type for newly created files + case CALL_COMPOSER_PICTURE: + // We don't know the exact image format, so this is as specific as we can be. + return "application/octet-stream"; default: throw new IllegalArgumentException("Unknown URI: " + uri); } @@ -494,6 +575,30 @@ public class CallLogProvider extends ContentProvider { " CUID=" + Binder.getCallingUid()); } waitForAccess(mReadAccessLatch); + int match = sURIMatcher.match(uri); + switch (match) { + case CALL_COMPOSER_PICTURE: { + String fileName = uri.getLastPathSegment(); + try { + return allocateNewCallComposerPicture(values, + CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority()), + fileName); + } catch (IOException e) { + throw new ParcelableException(e); + } + } + case CALL_COMPOSER_NEW_PICTURE: { + try { + return allocateNewCallComposerPicture(values, + CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority())); + } catch (IOException e) { + throw new ParcelableException(e); + } + } + default: + // Fall through and execute the rest of the method for ordinary call log insertions. + } + checkForSupportedColumns(sCallsProjectionMap, values); // Inserting a voicemail record through call_log requires the voicemail // permission and also requires the additional voicemail param set. @@ -518,6 +623,170 @@ public class CallLogProvider extends ContentProvider { return null; } + @Override + public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) + throws FileNotFoundException { + int match = sURIMatcher.match(uri); + if (match != CALL_COMPOSER_PICTURE) { + throw new UnsupportedOperationException("The call log provider only supports opening" + + " call composer pictures."); + } + int modeInt; + switch (mode) { + case "r": + modeInt = ParcelFileDescriptor.MODE_READ_ONLY; + break; + case "w": + modeInt = ParcelFileDescriptor.MODE_WRITE_ONLY; + break; + default: + throw new UnsupportedOperationException("The call log does not support opening" + + " a call composer picture with mode " + mode); + } + + try { + Path callComposerDir = getCallComposerPictureDirectory(getContext(), uri); + Path pictureFile = callComposerDir.resolve(uri.getLastPathSegment()); + if (Files.notExists(pictureFile)) { + throw new FileNotFoundException(uri.toString() + + " does not correspond to a valid file."); + } + return ParcelFileDescriptor.open(pictureFile.toFile(), modeInt); + } catch (IOException e) { + Log.e(TAG, "IOException while opening call composer file: " + e); + throw new RuntimeException(e); + } + } + + @Override + public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { + Log.i(TAG, "Fetching list of Uris to sync"); + if (!UserHandle.isSameApp(android.os.Process.myUid(), Binder.getCallingUid())) { + throw new SecurityException("call() functionality reserved" + + " for internal use by the call log."); + } + if (!GET_CALL_COMPOSER_IMAGE_URIS.equals(method)) { + throw new UnsupportedOperationException("Invalid method passed to call(): " + method); + } + if (!extras.containsKey(EXTRA_SINCE_DATE)) { + throw new IllegalArgumentException("SINCE_DATE required"); + } + if (!extras.containsKey(EXTRA_IS_SHADOW)) { + throw new IllegalArgumentException("IS_SHADOW required"); + } + if (!extras.containsKey(EXTRA_ALL_USERS_ONLY)) { + throw new IllegalArgumentException("ALL_USERS_ONLY required"); + } + boolean isShadow = extras.getBoolean(EXTRA_IS_SHADOW); + boolean allUsers = extras.getBoolean(EXTRA_ALL_USERS_ONLY); + long sinceDate = extras.getLong(EXTRA_SINCE_DATE); + + try { + Path queryDir = allUsers + ? getCallComposerAllUsersPictureDirectory(getContext(), isShadow) + : getCallComposerPictureDirectory(getContext(), isShadow); + List<Path> newestPics = new ArrayList<>(); + try (DirectoryStream<Path> dirStream = + Files.newDirectoryStream(queryDir, entry -> { + if (Files.isDirectory(entry)) { + return false; + } + FileTime createdAt = + (FileTime) Files.getAttribute(entry, "creationTime"); + return createdAt.toMillis() > sinceDate; + })) { + dirStream.forEach(newestPics::add); + } + List<Uri> fileUris = newestPics.stream().map((path) -> { + String fileName = path.getFileName().toString(); + // We don't need to worry about if it's for all users -- anything that's for + // all users is also stored in the regular location. + Uri base = isShadow ? CallLog.SHADOW_CALL_COMPOSER_PICTURE_URI + : CallLog.CALL_COMPOSER_PICTURE_URI; + return base.buildUpon().appendPath(fileName).build(); + }).collect(Collectors.toList()); + Bundle result = new Bundle(); + result.putParcelableList(EXTRA_RESULT_URIS, fileUris); + Log.i(TAG, "Will sync following Uris:" + fileUris); + return result; + } catch (IOException e) { + Log.e(TAG, "IOException while trying to fetch URI list: " + e); + return null; + } + } + + private static @NonNull Path getCallComposerPictureDirectory(Context context, Uri uri) + throws IOException { + boolean isShadow = CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority()); + return getCallComposerPictureDirectory(context, isShadow); + } + + private static @NonNull Path getCallComposerPictureDirectory(Context context, boolean isShadow) + throws IOException { + if (isShadow) { + context = context.createDeviceProtectedStorageContext(); + } + Path path = context.getFilesDir().toPath().resolve(CALL_COMPOSER_PICTURE_DIRECTORY_NAME); + if (!Files.isDirectory(path)) { + Files.createDirectory(path); + } + return path; + } + + private static @NonNull Path getCallComposerAllUsersPictureDirectory( + Context context, boolean isShadow) throws IOException { + Path pathToCallComposerDir = getCallComposerPictureDirectory(context, isShadow); + Path path = pathToCallComposerDir.resolve(CALL_COMPOSER_ALL_USERS_DIRECTORY_NAME); + if (!Files.isDirectory(path)) { + Files.createDirectory(path); + } + return path; + } + + private Uri allocateNewCallComposerPicture(ContentValues values, boolean isShadow) + throws IOException { + return allocateNewCallComposerPicture(values, isShadow, UUID.randomUUID().toString()); + } + + private Uri allocateNewCallComposerPicture(ContentValues values, + boolean isShadow, String fileName) throws IOException { + Uri baseUri = isShadow ? + CallLog.CALL_COMPOSER_PICTURE_URI.buildUpon() + .authority(CallLog.SHADOW_AUTHORITY).build() + : CallLog.CALL_COMPOSER_PICTURE_URI; + + boolean forAllUsers = values.containsKey(Calls.ADD_FOR_ALL_USERS) + && (values.getAsInteger(Calls.ADD_FOR_ALL_USERS) == 1); + Path pathToCallComposerDir = getCallComposerPictureDirectory(getContext(), isShadow); + + if (new StatFs(pathToCallComposerDir.toString()).getAvailableBytes() + < TelephonyManager.getMaximumCallComposerPictureSize()) { + return null; + } + Path pathToFile = pathToCallComposerDir.resolve(fileName); + Files.createFile(pathToFile); + + if (forAllUsers) { + // Create a symlink in a subdirectory for copying later. + Path allUsersDir = getCallComposerAllUsersPictureDirectory(getContext(), isShadow); + Files.createSymbolicLink(allUsersDir.resolve(fileName), pathToFile); + } + return baseUri.buildUpon().appendPath(fileName).build(); + } + + private int deleteCallComposerPicture(Uri uri) { + try { + Path pathToCallComposerDir = getCallComposerPictureDirectory(getContext(), uri); + String fileName = uri.getLastPathSegment(); + boolean successfulDelete = + Files.deleteIfExists(pathToCallComposerDir.resolve(fileName)); + return successfulDelete ? 1 : 0; + } catch (IOException e) { + Log.e(TAG, "IOException encountered deleting the call composer pics dir " + e); + return 0; + } + } + private int updateInternal(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (VERBOSE_LOGGING) { @@ -537,19 +806,8 @@ public class CallLogProvider extends ContentProvider { SelectionBuilder selectionBuilder = new SelectionBuilder(selection); checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); - - final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables(Tables.CALLS); - qb.setProjectionMap(sCallsProjectionMap); - qb.setStrict(true); - // If the caller doesn't have READ_VOICEMAIL, make sure they can't - // do any SQL shenanigans to get access to the voicemails. If the caller does have the - // READ_VOICEMAIL permission, then they have sufficient permissions to access any data in - // the database, so the strict check is unnecessary. - if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) { - qb.setStrictGrammar(true); - } - + boolean hasReadVoicemailPermission = mVoicemailPermissions.callerHasReadAccess( + getCallingPackage()); final SQLiteDatabase db = mDbHelper.getWritableDatabase(); final int matchedUriId = sURIMatcher.match(uri); switch (matchedUriId) { @@ -564,11 +822,8 @@ public class CallLogProvider extends ContentProvider { throw new UnsupportedOperationException("Cannot update URL: " + uri); } - int rowsUpdated = qb.update(db, values, selectionBuilder.build(), selectionArgs); - if (rowsUpdated > 0) { - DbModifierWithNotification.notifyCallLogChange(getContext()); - } - return rowsUpdated; + return createDatabaseModifier(db, hasReadVoicemailPermission).update(uri, Tables.CALLS, + values, selectionBuilder.build(), selectionArgs); } private int deleteInternal(Uri uri, String selection, String[] selectionArgs) { @@ -583,29 +838,18 @@ public class CallLogProvider extends ContentProvider { SelectionBuilder selectionBuilder = new SelectionBuilder(selection); checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); - final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables(Tables.CALLS); - qb.setProjectionMap(sCallsProjectionMap); - qb.setStrict(true); - // If the caller doesn't have READ_VOICEMAIL, make sure they can't - // do any SQL shenanigans to get access to the voicemails. If the caller does have the - // READ_VOICEMAIL permission, then they have sufficient permissions to access any data in - // the database, so the strict check is unnecessary. - if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) { - qb.setStrictGrammar(true); - } - + boolean hasReadVoicemailPermission = + mVoicemailPermissions.callerHasReadAccess(getCallingPackage()); final SQLiteDatabase db = mDbHelper.getWritableDatabase(); final int matchedUriId = sURIMatcher.match(uri); switch (matchedUriId) { case CALLS: - // TODO: Special case - We may want to forward the delete request on user 0 to the - // shadow provider too. - int deletedCount = qb.delete(db, selectionBuilder.build(), selectionArgs); - if (deletedCount > 0) { - DbModifierWithNotification.notifyCallLogChange(getContext()); - } - return deletedCount; + return createDatabaseModifier(db, hasReadVoicemailPermission).delete(Tables.CALLS, + selectionBuilder.build(), selectionArgs); + case CALL_COMPOSER_PICTURE: + // TODO(hallliu): implement deletion of file when the corresponding calllog entry + // gets deleted as well. + return deleteCallComposerPicture(uri); default: throw new UnsupportedOperationException("Cannot delete that URL: " + uri); } @@ -619,8 +863,9 @@ public class CallLogProvider extends ContentProvider { * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications * after the operation is performed. */ - private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { - return new DbModifierWithNotification(Tables.CALLS, db, getContext()); + private DatabaseModifier createDatabaseModifier(SQLiteDatabase db, boolean hasReadVoicemail) { + return new DbModifierWithNotification(Tables.CALLS, db, null, hasReadVoicemail, + getContext()); } /** @@ -762,8 +1007,68 @@ public class CallLogProvider extends ContentProvider { // delete all entries in shadow. cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)}); } + + try { + syncCallComposerPics(sourceUserId, sourceIsShadow, forAllUsersOnly, lastSyncTime); + } catch (Exception e) { + // Catch any exceptions to make sure we don't bring down the entire process if something + // goes wrong + StringWriter w = new StringWriter(); + PrintWriter pw = new PrintWriter(w); + e.printStackTrace(pw); + Log.e(TAG, "Caught exception syncing call composer pics: " + e + + "\n" + pw.toString()); + } } + private void syncCallComposerPics(int sourceUserId, boolean sourceIsShadow, + boolean forAllUsersOnly, long lastSyncTime) { + Log.i(TAG, "Syncing call composer pics -- source user=" + sourceUserId + "," + + " isShadow=" + sourceIsShadow + ", forAllUser=" + forAllUsersOnly); + ContentResolver contentResolver = getContext().getContentResolver(); + Bundle args = new Bundle(); + args.putLong(EXTRA_SINCE_DATE, lastSyncTime); + args.putBoolean(EXTRA_ALL_USERS_ONLY, forAllUsersOnly); + args.putBoolean(EXTRA_IS_SHADOW, sourceIsShadow); + Uri queryUri = ContentProvider.maybeAddUserId( + sourceIsShadow + ? CallLog.SHADOW_CALL_COMPOSER_PICTURE_URI + : CallLog.CALL_COMPOSER_PICTURE_URI, + sourceUserId); + Bundle result = contentResolver.call(queryUri, GET_CALL_COMPOSER_IMAGE_URIS, null, args); + if (result == null || !result.containsKey(EXTRA_RESULT_URIS)) { + Log.e(TAG, "Failed to sync call composer pics -- invalid return from call()"); + return; + } + List<Uri> urisToCopy = result.getParcelableArrayList(EXTRA_RESULT_URIS); + Log.i(TAG, "Syncing call composer pics -- got " + urisToCopy); + for (Uri uri : urisToCopy) { + try { + Uri uriWithUser = ContentProvider.maybeAddUserId(uri, sourceUserId); + Path newFilePath = getCallComposerPictureDirectory(getContext(), false) + .resolve(uri.getLastPathSegment()); + try (ParcelFileDescriptor remoteFile = contentResolver.openFile(uriWithUser, + "r", null); + OutputStream localOut = + Files.newOutputStream(newFilePath, StandardOpenOption.CREATE_NEW)) { + FileInputStream input = new FileInputStream(remoteFile.getFileDescriptor()); + byte[] buffer = new byte[1 << 14]; // 16kb + while (true) { + int numRead = input.read(buffer); + if (numRead < 0) { + break; + } + localOut.write(buffer, 0, numRead); + } + } + contentResolver.delete(uriWithUser, null); + } catch (IOException e) { + Log.e(TAG, "IOException while syncing call composer pics: " + e); + // Keep going and get as many as we can. + } + } + + } /** * Un-hides any hidden call log entries that are associated with the specified handle. * diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java index 8a1c88bb..09e3ff37 100644 --- a/src/com/android/providers/contacts/ContactDirectoryManager.java +++ b/src/com/android/providers/contacts/ContactDirectoryManager.java @@ -245,8 +245,7 @@ public class ContactDirectoryManager { Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms"); // Announce the change to listeners of the contacts authority - mContactsProvider.notifyChange(/* syncToNetwork =*/false, - /* syncToMetadataNetwork =*/false); + mContactsProvider.notifyChange(/* syncToNetwork =*/false); // We schedule a rescan if update(DIRECTORIES) is called while we're scanning all packages. if (mDirectoriesForceUpdated) { diff --git a/src/com/android/providers/contacts/ContactMetadataProvider.java b/src/com/android/providers/contacts/ContactMetadataProvider.java deleted file mode 100644 index 3cf7df2c..00000000 --- a/src/com/android/providers/contacts/ContactMetadataProvider.java +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Copyright (C) 2015 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; - -import android.content.ContentProvider; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.IContentProvider; -import android.content.OperationApplicationException; -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.Binder; -import android.provider.ContactsContract; -import android.provider.ContactsContract.MetadataSync; -import android.provider.ContactsContract.MetadataSyncState; -import android.text.TextUtils; -import android.util.Log; -import com.android.common.content.ProjectionMap; -import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncStateColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.ContactsDatabaseHelper.Views; -import com.android.providers.contacts.MetadataEntryParser.MetadataEntry; -import com.android.providers.contacts.util.SelectionBuilder; -import com.android.providers.contacts.util.UserUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; - -import static com.android.providers.contacts.ContactsProvider2.getLimit; -import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; - -/** - * Simple content provider to handle directing contact metadata specific calls. - */ -public class ContactMetadataProvider extends ContentProvider { - private static final String TAG = "ContactMetadata"; - private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); - private static final int METADATA_SYNC = 1; - private static final int METADATA_SYNC_ID = 2; - private static final int SYNC_STATE = 3; - - private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - static { - sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC); - sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID); - sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync_state", SYNC_STATE); - } - - private static final Map<String, String> sMetadataProjectionMap = ProjectionMap.builder() - .add(MetadataSync._ID) - .add(MetadataSync.RAW_CONTACT_BACKUP_ID) - .add(MetadataSync.ACCOUNT_TYPE) - .add(MetadataSync.ACCOUNT_NAME) - .add(MetadataSync.DATA_SET) - .add(MetadataSync.DATA) - .add(MetadataSync.DELETED) - .build(); - - private static final Map<String, String> sSyncStateProjectionMap =ProjectionMap.builder() - .add(MetadataSyncState._ID) - .add(MetadataSyncState.ACCOUNT_TYPE) - .add(MetadataSyncState.ACCOUNT_NAME) - .add(MetadataSyncState.DATA_SET) - .add(MetadataSyncState.STATE) - .build(); - - private ContactsDatabaseHelper mDbHelper; - private ContactsProvider2 mContactsProvider; - - private String mAllowedPackage; - - @Override - public boolean onCreate() { - final Context context = getContext(); - mDbHelper = getDatabaseHelper(context); - final IContentProvider iContentProvider = context.getContentResolver().acquireProvider( - ContactsContract.AUTHORITY); - final ContentProvider provider = ContentProvider.coerceToLocalContentProvider( - iContentProvider); - mContactsProvider = (ContactsProvider2) provider; - - mAllowedPackage = getContext().getResources().getString(R.string.metadata_sync_pacakge); - return true; - } - - protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { - return ContactsDatabaseHelper.getInstance(context); - } - - @VisibleForTesting - protected void setDatabaseHelper(final ContactsDatabaseHelper helper) { - mDbHelper = helper; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - - ensureCaller(); - - if (VERBOSE_LOGGING) { - Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + - " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + - " order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() + - " User=" + UserUtils.getCurrentUserHandle(getContext())); - } - final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - String limit = getLimit(uri); - - final SelectionBuilder selectionBuilder = new SelectionBuilder(selection); - - final int match = sURIMatcher.match(uri); - switch (match) { - case METADATA_SYNC: - setTablesAndProjectionMapForMetadata(qb); - break; - - case METADATA_SYNC_ID: { - setTablesAndProjectionMapForMetadata(qb); - selectionBuilder.addClause(getEqualityClause(MetadataSync._ID, - ContentUris.parseId(uri))); - break; - } - - case SYNC_STATE: - setTablesAndProjectionMapForSyncState(qb); - break; - default: - throw new IllegalArgumentException("Unknown URL " + uri); - } - - final SQLiteDatabase db = mDbHelper.getReadableDatabase(); - return qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, - null, sortOrder, limit); - } - - @Override - public String getType(Uri uri) { - int match = sURIMatcher.match(uri); - switch (match) { - case METADATA_SYNC: - return MetadataSync.CONTENT_TYPE; - case METADATA_SYNC_ID: - return MetadataSync.CONTENT_ITEM_TYPE; - case SYNC_STATE: - return MetadataSyncState.CONTENT_TYPE; - default: - throw new IllegalArgumentException("Unknown URI: " + uri); - } - } - - @Override - /** - * Insert or update if the raw is already existing. - */ - public Uri insert(Uri uri, ContentValues values) { - - ensureCaller(); - - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - final int matchedUriId = sURIMatcher.match(uri); - switch (matchedUriId) { - case METADATA_SYNC: - // Insert the new entry, and also parse the data column to update related - // tables. - final long metadataSyncId = updateOrInsertDataToMetadataSync(db, uri, values); - db.setTransactionSuccessful(); - return ContentUris.withAppendedId(uri, metadataSyncId); - case SYNC_STATE: - replaceAccountInfoByAccountId(uri, values); - final Long syncStateId = db.replace( - Tables.METADATA_SYNC_STATE, MetadataSyncColumns.ACCOUNT_ID, values); - db.setTransactionSuccessful(); - return ContentUris.withAppendedId(uri, syncStateId); - default: - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Calling contact metadata insert on an unknown/invalid URI", uri)); - } - } finally { - db.endTransaction(); - } - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - - ensureCaller(); - - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - final int matchedUriId = sURIMatcher.match(uri); - int numDeletes = 0; - switch (matchedUriId) { - case METADATA_SYNC: - Cursor c = db.query(Views.METADATA_SYNC, new String[]{MetadataSync._ID}, - selection, selectionArgs, null, null, null); - try { - while (c.moveToNext()) { - final long contactMetadataId = c.getLong(0); - numDeletes += db.delete(Tables.METADATA_SYNC, - MetadataSync._ID + "=" + contactMetadataId, null); - } - } finally { - c.close(); - } - db.setTransactionSuccessful(); - return numDeletes; - case SYNC_STATE: - c = db.query(Views.METADATA_SYNC_STATE, new String[]{MetadataSyncState._ID}, - selection, selectionArgs, null, null, null); - try { - while (c.moveToNext()) { - final long stateId = c.getLong(0); - numDeletes += db.delete(Tables.METADATA_SYNC_STATE, - MetadataSyncState._ID + "=" + stateId, null); - } - } finally { - c.close(); - } - db.setTransactionSuccessful(); - return numDeletes; - default: - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Calling contact metadata delete on an unknown/invalid URI", uri)); - } - } finally { - db.endTransaction(); - } - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - - ensureCaller(); - - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - final int matchedUriId = sURIMatcher.match(uri); - switch (matchedUriId) { - // Do not support update metadata sync by update() method. Please use insert(). - case SYNC_STATE: - // Only support update by account. - final Long accountId = replaceAccountInfoByAccountId(uri, values); - if (accountId == null) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Invalid identifier is found for accountId", uri)); - } - values.put(MetadataSyncColumns.ACCOUNT_ID, accountId); - // Insert a new row if it doesn't exist. - db.replace(Tables.METADATA_SYNC_STATE, null, values); - db.setTransactionSuccessful(); - return 1; - default: - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Calling contact metadata update on an unknown/invalid URI", uri)); - } - } finally { - db.endTransaction(); - } - } - - @Override - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) - throws OperationApplicationException { - - ensureCaller(); - - if (VERBOSE_LOGGING) { - Log.v(TAG, "applyBatch: " + operations.size() + " ops"); - } - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - ContentProviderResult[] results = super.applyBatch(operations); - db.setTransactionSuccessful(); - return results; - } finally { - db.endTransaction(); - } - } - - @Override - public int bulkInsert(Uri uri, ContentValues[] values) { - - ensureCaller(); - - if (VERBOSE_LOGGING) { - Log.v(TAG, "bulkInsert: " + values.length + " inserts"); - } - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - final int numValues = super.bulkInsert(uri, values); - db.setTransactionSuccessful(); - return numValues; - } finally { - db.endTransaction(); - } - } - - private void setTablesAndProjectionMapForMetadata(SQLiteQueryBuilder qb){ - qb.setTables(Views.METADATA_SYNC); - qb.setProjectionMap(sMetadataProjectionMap); - qb.setStrict(true); - } - - private void setTablesAndProjectionMapForSyncState(SQLiteQueryBuilder qb){ - qb.setTables(Views.METADATA_SYNC_STATE); - qb.setProjectionMap(sSyncStateProjectionMap); - qb.setStrict(true); - } - - /** - * Insert or update a non-deleted entry to MetadataSync table, and also parse the data column - * to update related tables for the raw contact. - * Returns new upserted metadataSyncId. - */ - private long updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values) { - final int matchUri = sURIMatcher.match(uri); - if (matchUri != METADATA_SYNC) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Calling contact metadata insert or update on an unknown/invalid URI", uri)); - } - - // Don't insert or update a deleted metadata. - Integer deleted = values.getAsInteger(MetadataSync.DELETED); - if (deleted != null && deleted != 0) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Cannot insert or update deleted metadata:" + values.toString(), uri)); - } - - // Check if data column is empty or null. - final String data = values.getAsString(MetadataSync.DATA); - if (TextUtils.isEmpty(data)) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Data column cannot be empty.", uri)); - } - - // Update or insert for backupId and account info. - final Long accountId = replaceAccountInfoByAccountId(uri, values); - final String rawContactBackupId = values.getAsString( - MetadataSync.RAW_CONTACT_BACKUP_ID); - // TODO (tingtingw): Consider a corner case: if there's raw with the same accountId and - // backupId, but deleted=1, (Deleted should be synced up to server and hard-deleted, but - // may be delayed.) In this case, should we not override it with delete=0? or should this - // be prevented by sync adapter side?. - deleted = 0; // Only insert or update non-deleted metadata - if (accountId == null) { - // Do nothing, just return. - return 0; - } - if (rawContactBackupId == null) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Invalid identifier is found: accountId=" + accountId + "; " + - "rawContactBackupId=" + rawContactBackupId, uri)); - } - - // Update if it exists, otherwise insert. - final long metadataSyncId = mDbHelper.upsertMetadataSync( - rawContactBackupId, accountId, data, deleted); - if (metadataSyncId <= 0) { - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Metadata upsertion failed. Values= " + values.toString(), uri)); - } - - // Parse the data column and update other tables. - // Data field will never be empty or null, since contacts prefs and usage stats - // have default values. - final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(data); - mContactsProvider.updateFromMetaDataEntry(db, metadataEntry); - - return metadataSyncId; - } - - /** - * Replace account_type, account_name and data_set with account_id. If a valid account_id - * cannot be found for this combination, return null. - */ - private Long replaceAccountInfoByAccountId(Uri uri, ContentValues values) { - String accountName = values.getAsString(MetadataSync.ACCOUNT_NAME); - String accountType = values.getAsString(MetadataSync.ACCOUNT_TYPE); - String dataSet = values.getAsString(MetadataSync.DATA_SET); - final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); - if (partialUri) { - // Throw when either account is incomplete. - throw new IllegalArgumentException(mDbHelper.exceptionMessage( - "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); - } - - final AccountWithDataSet account = AccountWithDataSet.get( - accountName, accountType, dataSet); - - final Long id = mDbHelper.getAccountIdOrNull(account); - if (id == null) { - return null; - } - - values.put(MetadataSyncColumns.ACCOUNT_ID, id); - // Only remove the account information once the account ID is extracted (since these - // fields are actually used by resolveAccountWithDataSet to extract the relevant ID). - values.remove(MetadataSync.ACCOUNT_NAME); - values.remove(MetadataSync.ACCOUNT_TYPE); - values.remove(MetadataSync.DATA_SET); - - return id; - } - - @VisibleForTesting - void ensureCaller() { - final String caller = getCallingPackage(); - if (mAllowedPackage.equals(caller)) { - return; // Okay. - } - throw new SecurityException("Caller " + caller + " can't access ContactMetadataProvider"); - } -} diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java index 2e5cdacf..7f4188d2 100644 --- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java +++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java @@ -16,12 +16,6 @@ package com.android.providers.contacts; -import com.android.internal.R.bool; -import com.android.providers.contacts.sqlite.DatabaseAnalyzer; -import com.android.providers.contacts.sqlite.SqlChecker; -import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException; -import com.android.providers.contacts.util.PropertyUtils; - import android.app.ActivityManager; import android.content.ContentResolver; import android.content.ContentValues; @@ -73,14 +67,13 @@ import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.DisplayPhoto; import android.provider.ContactsContract.FullNameStyle; import android.provider.ContactsContract.Groups; -import android.provider.ContactsContract.MetadataSync; -import android.provider.ContactsContract.MetadataSyncState; import android.provider.ContactsContract.PhoneticNameStyle; import android.provider.ContactsContract.PhotoFiles; import android.provider.ContactsContract.PinnedPositions; import android.provider.ContactsContract.ProviderStatus; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.Settings; +import android.provider.ContactsContract.SimAccount; import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItemPhotos; import android.provider.ContactsContract.StreamItems; @@ -98,17 +91,23 @@ import android.util.Log; import android.util.Slog; import com.android.common.content.SyncStateContentProviderHelper; +import com.android.internal.R.bool; import com.android.internal.annotations.VisibleForTesting; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; import com.android.providers.contacts.database.ContactsTableUtil; import com.android.providers.contacts.database.DeletedContactsTableUtil; import com.android.providers.contacts.database.MoreDatabaseUtils; +import com.android.providers.contacts.sqlite.DatabaseAnalyzer; +import com.android.providers.contacts.sqlite.SqlChecker; +import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException; import com.android.providers.contacts.util.NeededForTesting; +import com.android.providers.contacts.util.PropertyUtils; import java.io.PrintWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.Executor; @@ -143,9 +142,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { * 1200-1299 O * 1300-1399 P * 1400-1499 Q + * 1500-1599 S * </pre> */ - static final int DATABASE_VERSION = 1400; + static final int DATABASE_VERSION = 1501; private static final int MINIMUM_SUPPORTED_VERSION = 700; @VisibleForTesting @@ -189,8 +189,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { public static final String DIRECTORIES = "directories"; public static final String DEFAULT_DIRECTORY = "default_directory"; public static final String SEARCH_INDEX = "search_index"; - public static final String METADATA_SYNC = "metadata_sync"; - public static final String METADATA_SYNC_STATE = "metadata_sync_state"; public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris"; // This list of tables contains auto-incremented sequences. @@ -313,15 +311,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + " JOIN " + Tables.ACCOUNTS + " ON (" + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID + ")"; - - public static final String RAW_CONTACTS_JOIN_METADATA_SYNC = Tables.RAW_CONTACTS - + " JOIN " + Tables.METADATA_SYNC + " ON (" - + RawContactsColumns.CONCRETE_BACKUP_ID + "=" - + MetadataSyncColumns.CONCRETE_BACKUP_ID - + " AND " - + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" - + MetadataSyncColumns.CONCRETE_ACCOUNT_ID - + ")"; } public interface Joins { @@ -712,6 +701,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { String ACCOUNT_NAME = RawContacts.ACCOUNT_NAME; String ACCOUNT_TYPE = RawContacts.ACCOUNT_TYPE; String DATA_SET = RawContacts.DATA_SET; + String SIM_SLOT_INDEX = "sim_slot_index"; + String SIM_EF_TYPE = "sim_ef_type"; String CONCRETE_ACCOUNT_NAME = Tables.ACCOUNTS + "." + ACCOUNT_NAME; String CONCRETE_ACCOUNT_TYPE = Tables.ACCOUNTS + "." + ACCOUNT_TYPE; @@ -770,22 +761,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { public static final int USAGE_TYPE_INT_SHORT_TEXT = 2; } - public interface MetadataSyncColumns { - static final String CONCRETE_ID = Tables.METADATA_SYNC + "._id"; - static final String ACCOUNT_ID = "account_id"; - static final String CONCRETE_BACKUP_ID = Tables.METADATA_SYNC + "." + - MetadataSync.RAW_CONTACT_BACKUP_ID; - static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC + "." + ACCOUNT_ID; - static final String CONCRETE_DELETED = Tables.METADATA_SYNC + "." + - MetadataSync.DELETED; - } - - public interface MetadataSyncStateColumns { - static final String CONCRETE_ID = Tables.METADATA_SYNC_STATE + "._id"; - static final String ACCOUNT_ID = "account_id"; - static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC_STATE + "." + ACCOUNT_ID; - } - private interface EmailQuery { public static final String TABLE = Tables.DATA; @@ -1247,8 +1222,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { AccountsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + AccountsColumns.ACCOUNT_NAME + " TEXT, " + AccountsColumns.ACCOUNT_TYPE + " TEXT, " + - AccountsColumns.DATA_SET + " TEXT" + - ");"); + AccountsColumns.DATA_SET + " TEXT, " + + AccountsColumns.SIM_SLOT_INDEX + " INTEGER, " + + AccountsColumns.SIM_EF_TYPE + " INTEGER" + + ");"); // Note, there are two sets of the usage stat columns: LR_* and RAW_*. // RAW_* contain the real values, which clients can't access. The column names start @@ -1622,34 +1599,11 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { DataUsageStatColumns.USAGE_TYPE_INT + ");"); - db.execSQL("CREATE TABLE IF NOT EXISTS " - + Tables.METADATA_SYNC + " (" + - MetadataSync._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - MetadataSync.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," + - MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," + - MetadataSync.DATA + " TEXT," + - MetadataSync.DELETED + " INTEGER NOT NULL DEFAULT 0);"); - - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " + - Tables.METADATA_SYNC + " (" + - MetadataSync.RAW_CONTACT_BACKUP_ID + ", " + - MetadataSyncColumns.ACCOUNT_ID +");"); - db.execSQL("CREATE TABLE " + Tables.PRE_AUTHORIZED_URIS + " ("+ PreAuthorizedUris._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + PreAuthorizedUris.URI + " STRING NOT NULL, " + PreAuthorizedUris.EXPIRATION + " INTEGER NOT NULL DEFAULT 0);"); - db.execSQL("CREATE TABLE IF NOT EXISTS " - + Tables.METADATA_SYNC_STATE + " (" + - MetadataSyncState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - MetadataSyncStateColumns.ACCOUNT_ID + " INTEGER NOT NULL," + - MetadataSyncState.STATE + " BLOB);"); - - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_state_index ON " + - Tables.METADATA_SYNC_STATE + " (" + - MetadataSyncColumns.ACCOUNT_ID +");"); - // When adding new tables, be sure to also add size-estimates in updateSqliteStats createContactsViews(db); createGroupsView(db); @@ -2242,34 +2196,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"; db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect); - - String metadataSyncSelect = "SELECT " + - MetadataSyncColumns.CONCRETE_ID + ", " + - MetadataSync.RAW_CONTACT_BACKUP_ID + ", " + - AccountsColumns.ACCOUNT_NAME + ", " + - AccountsColumns.ACCOUNT_TYPE + ", " + - AccountsColumns.DATA_SET + ", " + - MetadataSync.DATA + ", " + - MetadataSync.DELETED + - " FROM " + Tables.METADATA_SYNC - + " JOIN " + Tables.ACCOUNTS + " ON (" - + MetadataSyncColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID - + ")"; - - db.execSQL("CREATE VIEW " + Views.METADATA_SYNC + " AS " + metadataSyncSelect); - - String metadataSyncStateSelect = "SELECT " + - MetadataSyncStateColumns.CONCRETE_ID + ", " + - AccountsColumns.ACCOUNT_NAME + ", " + - AccountsColumns.ACCOUNT_TYPE + ", " + - AccountsColumns.DATA_SET + ", " + - MetadataSyncState.STATE + - " FROM " + Tables.METADATA_SYNC_STATE - + " JOIN " + Tables.ACCOUNTS + " ON (" - + MetadataSyncStateColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID - + ")"; - - db.execSQL("CREATE VIEW " + Views.METADATA_SYNC_STATE + " AS " + metadataSyncStateSelect); } private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) { @@ -2655,6 +2581,19 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { oldVersion = 1400; } + if (isUpgradeRequired(oldVersion, newVersion, 1500)) { + db.execSQL("DROP TABLE IF EXISTS metadata_sync;"); + db.execSQL("DROP TABLE IF EXISTS metadata_sync_state;"); + upgradeViewsAndTriggers = true; + oldVersion = 1500; + } + + if (isUpgradeRequired(oldVersion, newVersion, 1501)) { + upgradeToVersion1501(db); + upgradeViewsAndTriggers = true; + oldVersion = 1501; + } + // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here // yet, until CallLogDatabaseHelper moves the data. @@ -3317,30 +3256,19 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } /** - * Add new metadata_sync table to cache the meta data on raw contacts level from server before - * they are merged into other CP2 tables. The data column is the blob column containing all - * the backed up metadata for this raw_contact. This table should only be used by metadata - * sync adapter. + * Used to add new metadata_sync table to cache the meta data on raw contacts level from server + * before they are merged into other CP2 tables. The table is not used any more. */ public void upgradeToVersion1104(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS metadata_sync;"); - db.execSQL("CREATE TABLE metadata_sync (" + - "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " + - "account_id INTEGER NOT NULL, data TEXT, deleted INTEGER NOT NULL DEFAULT 0);"); - db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" + - "raw_contact_backup_id, account_id);"); } /** - * Add new metadata_sync_state table to store the metadata sync state for a set of accounts. + * Used to add new metadata_sync_state table to store the metadata sync state for a set of + * accounts. The table is not used any more. */ public void upgradeToVersion1105(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS metadata_sync_state;"); - db.execSQL("CREATE TABLE metadata_sync_state (" + - "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "account_id INTEGER NOT NULL, state BLOB);"); - db.execSQL("CREATE UNIQUE INDEX metadata_sync_state_index ON metadata_sync_state (" + - "account_id);"); } public void upgradeToVersion1106(SQLiteDatabase db) { @@ -3439,6 +3367,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } } + private void upgradeToVersion1501(SQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE accounts ADD sim_slot_index INTEGER;"); + db.execSQL("ALTER TABLE accounts ADD sim_ef_type INTEGER;"); + } catch (SQLException 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 @@ -3693,9 +3629,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { updateIndexStats(db, Tables.DATA_USAGE_STAT, "data_usage_stat_index", "20 2 1"); - updateIndexStats(db, Tables.METADATA_SYNC, - "metadata_sync_index", "10000 1 1"); - // Tiny tables updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS, null, "10"); @@ -3716,9 +3649,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { updateIndexStats(db, "properties", "sqlite_autoindex_properties_1", "4 1"); - updateIndexStats(db, Tables.METADATA_SYNC_STATE, - "metadata_sync_state_index", "2 1 1"); - // Search index updateIndexStats(db, "search_index_docsize", null, "9000"); @@ -3980,6 +3910,37 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } /** + * Gets all SIM accounts in the accounts table. + */ + public List<SimAccount> getAllSimAccounts() { + final List<SimAccount> result = new ArrayList<>(); + final Cursor c = getReadableDatabase().rawQuery( + "SELECT DISTINCT " + AccountsColumns._ID + "," + + AccountsColumns.ACCOUNT_NAME + "," + + AccountsColumns.ACCOUNT_TYPE + "," + + AccountsColumns.SIM_SLOT_INDEX + "," + + AccountsColumns.SIM_EF_TYPE + " FROM " + Tables.ACCOUNTS, null); + try { + while (c.moveToNext()) { + if (c.isNull(3) || c.isNull(4)) { + // Invalid slot index or ef type + continue; + } + final int simSlot = c.getInt(3); + final int efType = c.getInt(4); + if (simSlot < 0 || !SimAccount.getValidEfTypes().contains(efType)) { + // Invalid slot index or ef type + continue; + } + result.add(new SimAccount(c.getString(1), c.getString(2), simSlot, efType)); + } + } finally { + c.close(); + } + return result; + } + + /** * @return ID of the specified account, or null if the account doesn't exist. */ public Long getAccountIdOrNull(AccountWithDataSet accountWithDataSet) { @@ -4042,6 +4003,69 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } /** + * This method will create a record in the accounts table. + * + * This must be used in a transaction, so there's no need for synchronization. + * + * @param simSlot Sim slot index of the account. Must be 0 or greater + * @param efType EF type of the account. Must be a value contained in {@link + * SimAccount#getValidEfTypes()} + * @throws IllegalArgumentException if the account name/type pair is already within the table. + * SIM accounts should have distinct names and types. + * And if simSlot is negative, or efType is not in {@link + * SimAccount#getValidEfTypes()} + */ + public long createSimAccountIdInTransaction(AccountWithDataSet accountWithDataSet, + int simSlot, int efType) { + if (simSlot < 0) { + throw new IllegalArgumentException("Sim slot is negative"); + } + if (!SimAccount.getValidEfTypes().contains(efType)) { + throw new IllegalArgumentException("Invalid EF type"); + } + if (accountWithDataSet == null || TextUtils.isEmpty(accountWithDataSet.getAccountName()) + || TextUtils.isEmpty(accountWithDataSet.getAccountType())) { + throw new IllegalArgumentException("Account is null or the name/type is empty"); + } + + Long id = getAccountIdOrNull(accountWithDataSet); + if (id != null) { + throw new IllegalArgumentException("Account already exists in the table"); + } + final SQLiteStatement insert = getWritableDatabase().compileStatement( + "INSERT INTO " + Tables.ACCOUNTS + + " (" + AccountsColumns.ACCOUNT_NAME + ", " + + AccountsColumns.ACCOUNT_TYPE + ", " + + AccountsColumns.DATA_SET + ", " + + AccountsColumns.SIM_SLOT_INDEX + ", " + + AccountsColumns.SIM_EF_TYPE + ") VALUES (?, ?, ?, ?, ?)"); + try { + DatabaseUtils.bindObjectToProgram(insert, 1, accountWithDataSet.getAccountName()); + DatabaseUtils.bindObjectToProgram(insert, 2, accountWithDataSet.getAccountType()); + DatabaseUtils.bindObjectToProgram(insert, 3, accountWithDataSet.getDataSet()); + DatabaseUtils.bindObjectToProgram(insert, 4, simSlot); + DatabaseUtils.bindObjectToProgram(insert, 5, efType); + id = insert.executeInsert(); + } finally { + insert.close(); + } + + return id; + } + + /** + * Deletes all rows in the accounts table with the given sim slot index + * + * @param simSlot Sim slot to remove accounts + * @return how many rows were deleted + */ + public int removeSimAccounts(int simSlot) { + final SQLiteDatabase db = getWritableDatabase(); + return db.delete(Tables.ACCOUNTS, AccountsColumns.SIM_SLOT_INDEX + "=?", + new String[]{String.valueOf(simSlot)}); + } + + /** * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts. */ public void updateAllVisible() { @@ -4989,22 +5013,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { new String[] {String.valueOf(contactId)}); } - public long upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted) { - final SQLiteStatement metadataSyncInsert = getWritableDatabase().compileStatement( - "INSERT OR REPLACE INTO " + Tables.METADATA_SYNC + "(" - + MetadataSync.RAW_CONTACT_BACKUP_ID + ", " - + MetadataSyncColumns.ACCOUNT_ID + ", " - + MetadataSync.DATA + "," - + MetadataSync.DELETED + ")" + - " VALUES (?,?,?,?)"); - metadataSyncInsert.bindString(1, backupId); - metadataSyncInsert.bindLong(2, accountId); - data = (data == null) ? "" : data; - metadataSyncInsert.bindString(3, data); - metadataSyncInsert.bindLong(4, deleted); - return metadataSyncInsert.executeInsert(); - } - public static void notifyProviderStatusChange(Context context) { context.getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, /* observer= */ null, /* syncToNetwork= */ false); diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index aad51c72..0c6e8192 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -16,9 +16,9 @@ package com.android.providers.contacts; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.accounts.Account; import android.accounts.AccountManager; @@ -34,6 +34,7 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.IContentService; +import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.content.SyncAdapterType; @@ -59,8 +60,11 @@ import android.net.Uri; import android.net.Uri.Builder; import android.os.AsyncTask; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.RemoteException; @@ -94,7 +98,6 @@ import android.provider.ContactsContract.DeletedContacts; import android.provider.ContactsContract.Directory; import android.provider.ContactsContract.DisplayPhoto; import android.provider.ContactsContract.Groups; -import android.provider.ContactsContract.MetadataSync; import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.PhotoFiles; import android.provider.ContactsContract.PinnedPositions; @@ -104,6 +107,8 @@ import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.RawContactsEntity; import android.provider.ContactsContract.SearchSnippets; import android.provider.ContactsContract.Settings; +import android.provider.ContactsContract.SimAccount; +import android.provider.ContactsContract.SimContacts; import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItemPhotos; import android.provider.ContactsContract.StreamItems; @@ -121,7 +126,6 @@ import android.util.Log; import com.android.common.content.ProjectionMap; import com.android.common.content.SyncStateContentProviderHelper; import com.android.common.io.MoreCloseables; -import com.android.i18n.phonenumbers.Phonenumber; import com.android.internal.util.ArrayUtils; import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; @@ -135,8 +139,6 @@ import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumn import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties; import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Joins; -import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncStateColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; @@ -153,11 +155,6 @@ import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Views; -import com.android.providers.contacts.MetadataEntryParser.AggregationData; -import com.android.providers.contacts.MetadataEntryParser.FieldData; -import com.android.providers.contacts.MetadataEntryParser.MetadataEntry; -import com.android.providers.contacts.MetadataEntryParser.RawContactInfo; -import com.android.providers.contacts.MetadataEntryParser.UsageStats; import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder; import com.android.providers.contacts.aggregation.AbstractContactAggregator; import com.android.providers.contacts.aggregation.AbstractContactAggregator.AggregationSuggestionParameter; @@ -173,6 +170,8 @@ import com.android.providers.contacts.enterprise.EnterprisePolicyGuard; import com.android.providers.contacts.util.Clock; import com.android.providers.contacts.util.ContactsPermissions; import com.android.providers.contacts.util.DbQueryUtils; +import com.android.providers.contacts.util.LogFields; +import com.android.providers.contacts.util.LogUtils; import com.android.providers.contacts.util.NeededForTesting; import com.android.providers.contacts.util.UserUtils; import com.android.vcard.VCardComposer; @@ -219,6 +218,8 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final String READ_PERMISSION = "android.permission.READ_CONTACTS"; private static final String WRITE_PERMISSION = "android.permission.WRITE_CONTACTS"; + private static final String MANAGE_SIM_ACCOUNTS_PERMISSION = + "android.contacts.permission.MANAGE_SIM_ACCOUNTS"; /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; @@ -259,6 +260,9 @@ public class ContactsProvider2 extends AbstractContactsProvider /** Limit for the maximum number of social stream items to store under a raw contact. */ private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5; + /** Rate limit (in milliseconds) for notify change. Do it as most once every 5 seconds. */ + private static final int NOTIFY_CHANGE_RATE_LIMIT = 5 * 1000; + /** Rate limit (in milliseconds) for photo cleanup. Do it at most once per day. */ private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; @@ -1453,13 +1457,14 @@ public class ContactsProvider2 extends AbstractContactsProvider private boolean mVisibleTouched = false; private boolean mSyncToNetwork; - private boolean mSyncToMetadataNetWork; private LocaleSet mCurrentLocales; private int mContactsAccountCount; private ContactsTaskScheduler mTaskScheduler; + private long mLastNotifyChange = 0; + private long mLastPhotoCleanup = 0; private FastScrollingIndexCache mFastScrollingIndexCache; @@ -1469,9 +1474,6 @@ public class ContactsProvider2 extends AbstractContactsProvider private int mFastScrollingIndexCacheMissCount; private long mTotalTimeFastScrollingIndexGenerate; - // MetadataSync flag. - private boolean mMetadataSyncEnabled; - // Enterprise members private EnterprisePolicyGuard mEnterprisePolicyGuard; @@ -1481,6 +1483,13 @@ public class ContactsProvider2 extends AbstractContactsProvider Log.v(TAG, "onCreate user=" + android.os.Process.myUserHandle().getIdentifier()); } + if (Build.IS_DEBUGGABLE) { + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() // for SqlLiteCursor + .detectLeakedClosableObjects() // for any Cursor + .penaltyLog() + .build()); + } if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start"); @@ -1515,9 +1524,6 @@ public class ContactsProvider2 extends AbstractContactsProvider mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext()); - mMetadataSyncEnabled = android.provider.Settings.Global.getInt( - getContext().getContentResolver(), Global.CONTACT_METADATA_SYNC_ENABLED, 0) == 1; - mContactsHelper = getDatabaseHelper(); mDbHelper.set(mContactsHelper); @@ -1971,8 +1977,7 @@ public class ContactsProvider2 extends AbstractContactsProvider ContentValues updateValues = new ContentValues(); updateValues.putNull(Photo.PHOTO_FILE_ID); updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), - updateValues, null, null, /* callerIsSyncAdapter =*/false, - /* callerIsMetadataSyncAdapter =*/false); + updateValues, null, null, /* callerIsSyncAdapter =*/false); } if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) { // For missing photos that were in stream item photos, just delete the @@ -2171,49 +2176,106 @@ public class ContactsProvider2 extends AbstractContactsProvider @Override public Uri insert(Uri uri, ContentValues values) { - waitForAccess(mWriteAccessLatch); + LogFields.Builder logBuilder = LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.INSERT) + .setUriType(sUriMatcher.match(uri)) + .setCallerIsSyncAdapter(readBooleanQueryParameter( + uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) + .setStartNanos(SystemClock.elapsedRealtimeNanos()); + Uri resultUri = null; - mContactsHelper.validateContentValues(getCallingPackage(), values); + try { + waitForAccess(mWriteAccessLatch); - if (mapsToProfileDbWithInsertedValues(uri, values)) { - switchToProfileMode(); - return mProfileProvider.insert(uri, values); + mContactsHelper.validateContentValues(getCallingPackage(), values); + + if (mapsToProfileDbWithInsertedValues(uri, values)) { + switchToProfileMode(); + resultUri = mProfileProvider.insert(uri, values); + return resultUri; + } + switchToContactMode(); + resultUri = super.insert(uri, values); + return resultUri; + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log( + logBuilder.setResultUri(resultUri).setResultCount(resultUri == null ? 0 : 1) + .build()); } - switchToContactMode(); - return super.insert(uri, values); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - waitForAccess(mWriteAccessLatch); + LogFields.Builder logBuilder = LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.UPDATE) + .setUriType(sUriMatcher.match(uri)) + .setCallerIsSyncAdapter(readBooleanQueryParameter( + uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) + .setStartNanos(SystemClock.elapsedRealtimeNanos()); + int updates = 0; - mContactsHelper.validateContentValues(getCallingPackage(), values); - mContactsHelper.validateSql(getCallingPackage(), selection); + try { + waitForAccess(mWriteAccessLatch); - if (mapsToProfileDb(uri)) { - switchToProfileMode(); - return mProfileProvider.update(uri, values, selection, selectionArgs); + mContactsHelper.validateContentValues(getCallingPackage(), values); + mContactsHelper.validateSql(getCallingPackage(), selection); + + if (mapsToProfileDb(uri)) { + switchToProfileMode(); + updates = mProfileProvider.update(uri, values, selection, selectionArgs); + return updates; + } + switchToContactMode(); + updates = super.update(uri, values, selection, selectionArgs); + return updates; + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log(logBuilder.setResultCount(updates).build()); } - switchToContactMode(); - return super.update(uri, values, selection, selectionArgs); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - waitForAccess(mWriteAccessLatch); + LogFields.Builder logBuilder = LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.DELETE) + .setUriType(sUriMatcher.match(uri)) + .setCallerIsSyncAdapter(readBooleanQueryParameter( + uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) + .setStartNanos(SystemClock.elapsedRealtimeNanos()); + int deletes = 0; - mContactsHelper.validateSql(getCallingPackage(), selection); + try { + waitForAccess(mWriteAccessLatch); - if (mapsToProfileDb(uri)) { - switchToProfileMode(); - return mProfileProvider.delete(uri, selection, selectionArgs); + mContactsHelper.validateSql(getCallingPackage(), selection); + + if (mapsToProfileDb(uri)) { + switchToProfileMode(); + deletes = mProfileProvider.delete(uri, selection, selectionArgs); + return deletes; + } + switchToContactMode(); + deletes = super.delete(uri, selection, selectionArgs); + return deletes; + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log(logBuilder.setResultCount(deletes).build()); } - switchToContactMode(); - return super.delete(uri, selection, selectionArgs); } @Override public Bundle call(String method, String arg, Bundle extras) { + LogFields.Builder logBuilder = + LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.CALL) + .setStartNanos(SystemClock.elapsedRealtimeNanos()); waitForAccess(mReadAccessLatch); switchToContactMode(); if (Authorization.AUTHORIZATION_METHOD.equals(method)) { @@ -2236,6 +2298,95 @@ public class ContactsProvider2 extends AbstractContactsProvider } undemoteContact(mDbHelper.get().getWritableDatabase(), id); return null; + } else if (SimContacts.ADD_SIM_ACCOUNT_METHOD.equals(method)) { + ContactsPermissions.enforceCallingOrSelfPermission(getContext(), + MANAGE_SIM_ACCOUNTS_PERMISSION); + + final String accountName = extras.getString(SimContacts.KEY_ACCOUNT_NAME); + final String accountType = extras.getString(SimContacts.KEY_ACCOUNT_TYPE); + final int simSlot = extras.getInt(SimContacts.KEY_SIM_SLOT_INDEX, -1); + final int efType = extras.getInt(SimContacts.KEY_SIM_EF_TYPE, -1); + if (simSlot < 0) { + throw new IllegalArgumentException("Sim slot is negative"); + } + if (!SimAccount.getValidEfTypes().contains(efType)) { + throw new IllegalArgumentException("Invalid EF type"); + } + if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("Account name or type is empty"); + } + + long resultId = -1; + final Bundle response = new Bundle(); + final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); + db.beginTransaction(); + try { + resultId = mDbHelper.get().createSimAccountIdInTransaction( + AccountWithDataSet.get(accountName, accountType, null), simSlot, efType); + db.setTransactionSuccessful(); + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log( + logBuilder + .setMethodCall(LogUtils.MethodCall.ADD_SIM_ACCOUNTS) + .setResultCount(resultId > -1 ? 1 : 0) + .build()); + db.endTransaction(); + } + + getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); + return response; + } else if (SimContacts.REMOVE_SIM_ACCOUNT_METHOD.equals(method)) { + ContactsPermissions.enforceCallingOrSelfPermission( + getContext(), MANAGE_SIM_ACCOUNTS_PERMISSION); + + final int simSlot = extras.getInt(SimContacts.KEY_SIM_SLOT_INDEX, -1); + if (simSlot < 0) { + throw new IllegalArgumentException("Sim slot is negative"); + } + + int removedCount = 0; + final Bundle response = new Bundle(); + final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); + db.beginTransaction(); + try { + removedCount = mDbHelper.get().removeSimAccounts(simSlot); + scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); + db.setTransactionSuccessful(); + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log( + logBuilder + .setMethodCall(LogUtils.MethodCall.REMOVE_SIM_ACCOUNTS) + .setResultCount(removedCount) + .build()); + db.endTransaction(); + } + getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); + return response; + } else if (SimContacts.QUERY_SIM_ACCOUNTS_METHOD.equals(method)) { + ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION); + final Bundle response = new Bundle(); + int accountsCount = 0; + try { + final List<SimAccount> simAccounts = mDbHelper.get().getAllSimAccounts(); + response.putParcelableList(SimContacts.KEY_SIM_ACCOUNTS, simAccounts); + accountsCount = simAccounts.size(); + return response; + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log( + logBuilder + .setMethodCall(LogUtils.MethodCall.GET_SIM_ACCOUNTS) + .setResultCount(accountsCount) + .build()); + } } return null; } @@ -2455,11 +2606,6 @@ public class ContactsProvider2 extends AbstractContactsProvider mTransactionContext.get().clearExceptSearchIndexUpdates(); } - @VisibleForTesting - void setMetadataSyncForTest(boolean enabled) { - mMetadataSyncEnabled = enabled; - } - /** * Appends comma separated IDs. * @param ids Should not be empty @@ -2474,14 +2620,44 @@ public class ContactsProvider2 extends AbstractContactsProvider @Override protected void notifyChange() { - notifyChange(mSyncToNetwork, mSyncToMetadataNetWork); + notifyChange(mSyncToNetwork); mSyncToNetwork = false; - mSyncToMetadataNetWork = false; } - protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) { + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Runnable mChangeNotifier = () -> { + Log.v(TAG, "Scheduled notifyChange started."); + mLastNotifyChange = System.currentTimeMillis(); getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, + false); + }; + + protected void notifyChange(boolean syncToNetwork) { + if (syncToNetwork) { + // Changes to sync to network won't be rate limited. + getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, syncToNetwork); + } else { + // Rate limit the changes which are not to sync to network. + long currentTimeMillis = System.currentTimeMillis(); + + mHandler.removeCallbacks(mChangeNotifier); + if (currentTimeMillis > mLastNotifyChange + NOTIFY_CHANGE_RATE_LIMIT) { + // Notify change immediately, since it has been a while since last notify. + mLastNotifyChange = currentTimeMillis; + getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, + false); + } else { + // Schedule a delayed notification, to ensure the very last notifyChange will be + // executed. + // Delay is set to two-fold of rate limit, and the subsequent notifyChange called + // (if ever) between the (NOTIFY_CHANGE_RATE_LIMIT, 2 * NOTIFY_CHANGE_RATE_LIMIT) + // time window, will cancel this delayed notification. + // The delayed notification is only expected to run if notifyChange is not invoked + // between the above time window. + mHandler.postDelayed(mChangeNotifier, NOTIFY_CHANGE_RATE_LIMIT * 2); + } + } } protected void setProviderStatus(int status) { @@ -3039,8 +3215,8 @@ public class ContactsProvider2 extends AbstractContactsProvider Uri dataUri = inProfileMode() ? Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY) : Data.CONTENT_URI; - Cursor c = query(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS, - selection, selectionArgs, null); + Cursor c = queryInternal(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS, + selection, selectionArgs, null, null); try { while(c.moveToNext()) { long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID); @@ -3067,8 +3243,8 @@ public class ContactsProvider2 extends AbstractContactsProvider // Note that the query will return data according to the access restrictions, // so we don't need to worry about deleting data we don't have permission to read. mSelectionArgs1[0] = String.valueOf(dataId); - Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?", - mSelectionArgs1, null); + Cursor c = queryInternal(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, + Data._ID + "=?", mSelectionArgs1, null, null); try { if (!c.moveToFirst()) { @@ -3895,8 +4071,7 @@ public class ContactsProvider2 extends AbstractContactsProvider values.put(RawContactsColumns.AGGREGATION_NEEDED, 1); values.putNull(RawContacts.CONTACT_ID); values.put(RawContacts.DIRTY, 1); - return updateRawContact(db, rawContactId, values, callerIsSyncAdapter, - /* callerIsMetadataSyncAdapter =*/false); + return updateRawContact(db, rawContactId, values, callerIsSyncAdapter); } static int deleteDataUsage(SQLiteDatabase db) { @@ -3998,8 +4173,7 @@ public class ContactsProvider2 extends AbstractContactsProvider String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") + (selection == null ? "" : " AND " + selection); - count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter, - /* callerIsMetadataSyncAdapter =*/false); + count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); break; } @@ -4007,8 +4181,7 @@ public class ContactsProvider2 extends AbstractContactsProvider case PROFILE_DATA: { invalidateFastScrollingIndexCache(); count = updateData(uri, values, appendAccountToSelection(uri, selection), - selectionArgs, callerIsSyncAdapter, - /* callerIsMetadataSyncAdapter =*/false); + selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } @@ -4021,8 +4194,7 @@ public class ContactsProvider2 extends AbstractContactsProvider case CALLABLES_ID: case POSTALS_ID: { invalidateFastScrollingIndexCache(); - count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter, - /* callerIsMetadataSyncAdapter =*/false); + count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); if (count > 0) { mSyncToNetwork |= !callerIsSyncAdapter; } @@ -4075,8 +4247,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } case AGGREGATION_EXCEPTIONS: { - count = updateAggregationException(db, values, - /* callerIsMetadataSyncAdapter =*/false); + count = updateAggregationException(db, values); invalidateFastScrollingIndexCache(); break; } @@ -4402,8 +4573,7 @@ public class ContactsProvider2 extends AbstractContactsProvider try { while (cursor.moveToNext()) { long rawContactId = cursor.getLong(0); - updateRawContact(db, rawContactId, values, callerIsSyncAdapter, - /* callerIsMetadataSyncAdapter =*/false); + updateRawContact(db, rawContactId, values, callerIsSyncAdapter); count++; } } finally { @@ -4436,7 +4606,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values, - boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + boolean callerIsSyncAdapter) { final String selection = RawContactsColumns.CONCRETE_ID + " = ?"; mSelectionArgs1[0] = Long.toString(rawContactId); @@ -4567,8 +4737,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } private int updateData(Uri uri, ContentValues inputValues, String selection, - String[] selectionArgs, boolean callerIsSyncAdapter, - boolean callerIsMetadataSyncAdapter) { + String[] selectionArgs, boolean callerIsSyncAdapter) { final ContentValues values = new ContentValues(inputValues); values.remove(Data._ID); @@ -4594,7 +4763,7 @@ public class ContactsProvider2 extends AbstractContactsProvider selection, selectionArgs, null, -1 /* directory ID */, null); try { while(c.moveToNext()) { - count += updateData(values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter); + count += updateData(values, c, callerIsSyncAdapter); } } finally { c.close(); @@ -4610,8 +4779,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter, - boolean callerIsMetadataSyncAdapter) { + private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { if (values.size() == 0) { return 0; } @@ -4626,7 +4794,7 @@ public class ContactsProvider2 extends AbstractContactsProvider DataRowHandler rowHandler = getDataRowHandler(mimeType); boolean updated = rowHandler.update(db, mTransactionContext.get(), values, c, - callerIsSyncAdapter, callerIsMetadataSyncAdapter); + callerIsSyncAdapter); if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); } @@ -4744,8 +4912,7 @@ public class ContactsProvider2 extends AbstractContactsProvider return rslt; } - private int updateAggregationException(SQLiteDatabase db, ContentValues values, - boolean callerIsMetadataSyncAdapter) { + private int updateAggregationException(SQLiteDatabase db, ContentValues values) { Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE); Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1); Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2); @@ -4868,27 +5035,6 @@ public class ContactsProvider2 extends AbstractContactsProvider return result; } - private long searchRawContactIdForRawContactInfo(SQLiteDatabase db, - RawContactInfo rawContactInfo) { - if (rawContactInfo == null) { - return 0; - } - final String backupId = rawContactInfo.mBackupId; - final String accountType = rawContactInfo.mAccountType; - final String accountName = rawContactInfo.mAccountName; - final String dataSet = rawContactInfo.mDataSet; - ContentValues values = new ContentValues(); - values.put(AccountsColumns.ACCOUNT_TYPE, accountType); - values.put(AccountsColumns.ACCOUNT_NAME, accountName); - if (dataSet != null) { - values.put(AccountsColumns.DATA_SET, dataSet); - } - - final long accountId = replaceAccountInfoByAccountId(RawContacts.CONTENT_URI, values); - final long rawContactId = queryRawContactId(db, backupId, accountId); - return rawContactId; - } - interface AggregationExceptionQuery { String TABLE = Tables.AGGREGATION_EXCEPTIONS; String[] COLUMNS = new String[] { @@ -4925,80 +5071,6 @@ public class ContactsProvider2 extends AbstractContactsProvider return aggregationRawContactIds; } - /** - * Update RawContact, Data, DataUsageStats, AggregationException tables from MetadataEntry. - */ - @NeededForTesting - void updateFromMetaDataEntry(SQLiteDatabase db, MetadataEntry metadataEntry) { - final RawContactInfo rawContactInfo = metadataEntry.mRawContactInfo; - final long rawContactId = searchRawContactIdForRawContactInfo(db, rawContactInfo); - if (rawContactId == 0) { - return; - } - - ContentValues rawContactValues = new ContentValues(); - rawContactValues.put(RawContacts.SEND_TO_VOICEMAIL, metadataEntry.mSendToVoicemail); - rawContactValues.put(RawContacts.STARRED, metadataEntry.mStarred); - rawContactValues.put(RawContacts.PINNED, metadataEntry.mPinned); - updateRawContact(db, rawContactId, rawContactValues, /* callerIsSyncAdapter =*/true, - /* callerIsMetadataSyncAdapter =*/true); - - // Update Data and DataUsageStats table. - for (int i = 0; i < metadataEntry.mFieldDatas.size(); i++) { - final FieldData fieldData = metadataEntry.mFieldDatas.get(i); - final String dataHashId = fieldData.mDataHashId; - final ArrayList<Long> dataIds = queryDataId(db, rawContactId, dataHashId); - - for (long dataId : dataIds) { - // Update is_primary and is_super_primary. - ContentValues dataValues = new ContentValues(); - dataValues.put(Data.IS_PRIMARY, fieldData.mIsPrimary ? 1 : 0); - dataValues.put(Data.IS_SUPER_PRIMARY, fieldData.mIsSuperPrimary ? 1 : 0); - updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), - dataValues, null, null, /* callerIsSyncAdapter =*/true, - /* callerIsMetadataSyncAdapter =*/true); - - } - } - - // Update AggregationException table. - final Set<Long> aggregationRawContactIdsInServer = new ArraySet<>(); - for (int i = 0; i < metadataEntry.mAggregationDatas.size(); i++) { - final AggregationData aggregationData = metadataEntry.mAggregationDatas.get(i); - final int typeInt = getAggregationType(aggregationData.mType, null); - final RawContactInfo aggregationContact1 = aggregationData.mRawContactInfo1; - final RawContactInfo aggregationContact2 = aggregationData.mRawContactInfo2; - final long rawContactId1 = searchRawContactIdForRawContactInfo(db, aggregationContact1); - final long rawContactId2 = searchRawContactIdForRawContactInfo(db, aggregationContact2); - if (rawContactId1 == 0 || rawContactId2 == 0) { - continue; - } - ContentValues values = new ContentValues(); - values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); - values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); - values.put(AggregationExceptions.TYPE, typeInt); - updateAggregationException(db, values, /* callerIsMetadataSyncAdapter =*/true); - if (rawContactId1 != rawContactId) { - aggregationRawContactIdsInServer.add(rawContactId1); - } - if (rawContactId2 != rawContactId) { - aggregationRawContactIdsInServer.add(rawContactId2); - } - } - - // Delete AggregationExceptions from CP2 if it doesn't exist in server side. - Set<Long> aggregationRawContactIdsInLocal = queryAggregationRawContactIds(db, rawContactId); - Set<Long> rawContactIdsToBeDeleted = com.google.common.collect.Sets.difference( - aggregationRawContactIdsInLocal, aggregationRawContactIdsInServer); - for (Long deleteRawContactId : rawContactIdsToBeDeleted) { - ContentValues values = new ContentValues(); - values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId); - values.put(AggregationExceptions.RAW_CONTACT_ID2, deleteRawContactId); - values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC); - updateAggregationException(db, values, /* callerIsMetadataSyncAdapter =*/true); - } - } - /** return serialized version of {@code accounts} */ @VisibleForTesting static String accountsToString(Set<Account> accounts) { @@ -5093,12 +5165,14 @@ public class ContactsProvider2 extends AbstractContactsProvider // All accounts that are used in raw_contacts and/or groups. final Set<AccountWithDataSet> knownAccountsWithDataSets = dbHelper.getAllAccountsWithDataSets(); - + // All known SIM accounts + final List<SimAccount> simAccounts = getDatabaseHelper().getAllSimAccounts(); // Find the accounts that have been removed. final List<AccountWithDataSet> accountsWithDataSetsToDelete = Lists.newArrayList(); for (AccountWithDataSet knownAccountWithDataSet : knownAccountsWithDataSets) { if (knownAccountWithDataSet.isLocalAccount() - || knownAccountWithDataSet.inSystemAccounts(systemAccounts)) { + || knownAccountWithDataSet.inSystemAccounts(systemAccounts) + || knownAccountWithDataSet.inSimAccounts(simAccounts)) { continue; } accountsWithDataSetsToDelete.add(knownAccountWithDataSet); @@ -5108,7 +5182,6 @@ public class ContactsProvider2 extends AbstractContactsProvider for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) { final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet); - // getAccountIdOrNull() really shouldn't return null here, but just in case... if (accountIdOrNull != null) { final String accountId = Long.toString(accountIdOrNull); final String[] accountIdParams = @@ -5141,14 +5214,6 @@ public class ContactsProvider2 extends AbstractContactsProvider " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)", accountIdParams); - db.execSQL( - "DELETE FROM " + Tables.METADATA_SYNC + - " WHERE " + MetadataSyncColumns.ACCOUNT_ID + " = ?", - accountIdParams); - db.execSQL( - "DELETE FROM " + Tables.METADATA_SYNC_STATE + - " WHERE " + MetadataSyncStateColumns.ACCOUNT_ID + " = ?", - accountIdParams); // Delta API is only needed for regular contacts. if (!inProfileMode()) { @@ -5349,6 +5414,29 @@ public class ContactsProvider2 extends AbstractContactsProvider @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + LogFields.Builder logBuilder = LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.QUERY) + .setUriType(sUriMatcher.match(uri)) + .setCallerIsSyncAdapter(readBooleanQueryParameter( + uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) + .setStartNanos(SystemClock.elapsedRealtimeNanos()); + + Cursor cursor = null; + try { + cursor = queryInternal(uri, projection, selection, selectionArgs, sortOrder, + cancellationSignal); + return cursor; + } catch (Exception e) { + logBuilder.setException(e); + throw e; + } finally { + LogUtils.log( + logBuilder.setResultCount(cursor == null ? 0 : cursor.getCount()).build()); + } + } + + private Cursor queryInternal(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { if (VERBOSE_LOGGING) { Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java index 235edfed..a82ce34f 100644 --- a/src/com/android/providers/contacts/DataRowHandler.java +++ b/src/com/android/providers/contacts/DataRowHandler.java @@ -121,7 +121,6 @@ public abstract class DataRowHandler { if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) { final long mimeTypeId = getMimeTypeId(); mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); - txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false); // We also have to make sure that no other data item on this raw_contact is // configured super primary @@ -154,13 +153,11 @@ public abstract class DataRowHandler { * @return true if update changed something */ public boolean update(SQLiteDatabase db, TransactionContext txContext, - ContentValues values, Cursor c, boolean callerIsSyncAdapter, - boolean callerIsMetadataSyncAdapter) { + ContentValues values, Cursor c, boolean callerIsSyncAdapter) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); - handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId, - callerIsMetadataSyncAdapter); + handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId); handleHashIdForUpdate(values, dataId); if (values.size() > 0) { @@ -251,15 +248,13 @@ public abstract class DataRowHandler { * configured correctly */ private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values, - long dataId, long rawContactId, boolean callerIsMetadataSyncAdapter) { + long dataId, long rawContactId) { final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null; final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null; // Nothing to do? Bail out early if (!hasPrimary && !hasSuperPrimary) return; - txContext.markRawContactMetadataDirty(rawContactId, callerIsMetadataSyncAdapter); - final long mimeTypeId = getMimeTypeId(); // Check if we want to clear values @@ -325,7 +320,6 @@ public abstract class DataRowHandler { db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); if (count != 0 && primary) { fixPrimary(db, rawContactId); - txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false); } if (hasSearchableData()) { diff --git a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java index 5ae3a01f..063fcdb2 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java +++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java @@ -49,15 +49,14 @@ public class DataRowHandlerForCommonDataKind extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final ContentValues augmented = getAugmentedValues(db, dataId, values); if (augmented == null) { // No change return false; } enforceTypeAndLabel(augmented); - return super.update(db, txContext, values, c, callerIsSyncAdapter, - callerIsMetadataSyncAdapter); + return super.update(db, txContext, values, c, callerIsSyncAdapter); } /** diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java index 3c7311f0..539c9596 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java +++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java @@ -50,8 +50,8 @@ public class DataRowHandlerForEmail extends DataRowHandlerForCommonDataKind { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + Cursor c, boolean callerIsSyncAdapter) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java index b1c40497..3f310b18 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java +++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java @@ -86,11 +86,11 @@ public class DataRowHandlerForGroupMembership extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId); resolveGroupSourceIdInValues(txContext, rawContactId, db, values, false); - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } boolean isStarred = hasFavoritesGroupMembership(db, rawContactId); diff --git a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java index 4d4a33f6..32e9757e 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java +++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java @@ -46,9 +46,9 @@ public class DataRowHandlerForIdentity extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { - super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter); + super.update(db, txContext, values, c, callerIsSyncAdapter); // Identity affects aggregation. final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java index cc85c2b1..03b96a3a 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java +++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java @@ -52,11 +52,11 @@ public class DataRowHandlerForNickname extends DataRowHandlerForCommonDataKind { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java index 5b69fe3c..66a3b1bd 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java +++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java @@ -51,8 +51,8 @@ public class DataRowHandlerForOrganization extends DataRowHandlerForCommonDataKi @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + Cursor c, boolean callerIsSyncAdapter) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java index 7bbac689..052252e1 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java @@ -58,10 +58,10 @@ public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKin @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { fillNormalizedNumber(values); - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java index 3d28b05c..532a8521 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java @@ -76,7 +76,7 @@ public class DataRowHandlerForPhoto extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); if (values.containsKey(SKIP_PROCESSING_KEY)) { @@ -89,7 +89,7 @@ public class DataRowHandlerForPhoto extends DataRowHandler { } // Do the actual update. - if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) { + if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java index b80f759d..044e9726 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java +++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java @@ -63,7 +63,7 @@ public class DataRowHandlerForStructuredName extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); @@ -74,7 +74,7 @@ public class DataRowHandlerForStructuredName extends DataRowHandler { fixStructuredNameComponents(augmented, values); - super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter); + super.update(db, txContext, values, c, callerIsSyncAdapter); if (values.containsKey(StructuredName.DISPLAY_NAME)) { augmented.putAll(values); String name = augmented.getAsString(StructuredName.DISPLAY_NAME); diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java index 235bbd7c..7fc97b7a 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java +++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java @@ -60,7 +60,7 @@ public class DataRowHandlerForStructuredPostal extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, - Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) { + Cursor c, boolean callerIsSyncAdapter) { final long dataId = c.getLong(DataUpdateQuery._ID); final ContentValues augmented = getAugmentedValues(db, dataId, values); if (augmented == null) { // No change @@ -68,7 +68,7 @@ public class DataRowHandlerForStructuredPostal extends DataRowHandler { } fixStructuredPostalComponents(augmented, values); - super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter); + super.update(db, txContext, values, c, callerIsSyncAdapter); return true; } diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java index 03ebd1f1..dc74c0ab 100644 --- a/src/com/android/providers/contacts/DbModifierWithNotification.java +++ b/src/com/android/providers/contacts/DbModifierWithNotification.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.database.Cursor; import android.database.DatabaseUtils.InsertHelper; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; import android.provider.CallLog.Calls; @@ -65,6 +66,7 @@ public class DbModifierWithNotification implements DatabaseModifier { Voicemails.DELETED + " == 0"; private final String mTableName; private final SQLiteDatabase mDb; + private final boolean mHasReadVoicemailPermission; private final InsertHelper mInsertHelper; private final Context mContext; private final Uri mBaseUri; @@ -86,8 +88,14 @@ public class DbModifierWithNotification implements DatabaseModifier { private DbModifierWithNotification(String tableName, SQLiteDatabase db, InsertHelper insertHelper, Context context) { + this(tableName, db, insertHelper, true /* hasReadVoicemail */, context); + } + + public DbModifierWithNotification(String tableName, SQLiteDatabase db, + InsertHelper insertHelper, boolean hasReadVoicemailPermission, Context context) { mTableName = tableName; mDb = db; + mHasReadVoicemailPermission = hasReadVoicemailPermission; mInsertHelper = insertHelper; mContext = context; mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ? @@ -196,7 +204,16 @@ public class DbModifierWithNotification implements DatabaseModifier { if (values.isEmpty()) { return 0; } - int count = mDb.update(table, values, whereClause, whereArgs); + + final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(mTableName); + qb.setProjectionMap(CallLogProvider.sCallsProjectionMap); + qb.setStrict(true); + if (!mHasReadVoicemailPermission) { + qb.setStrictGrammar(true); + } + int count = qb.update(mDb, values, whereClause, whereArgs); + if (count > 0 && isVoicemailContent || Tables.VOICEMAIL_STATUS.equals(table)) { notifyVoicemailChange(mBaseUri, packagesModified); } @@ -269,14 +286,23 @@ public class DbModifierWithNotification implements DatabaseModifier { // If the deletion is being made by the package that inserted the voicemail or by // CP2 (cleanup after uninstall), then we don't need to wait for sync, so just delete it. final int count; + + final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(mTableName); + qb.setProjectionMap(CallLogProvider.sCallsProjectionMap); + qb.setStrict(true); + if (!mHasReadVoicemailPermission) { + qb.setStrictGrammar(true); + } + if (mIsCallsTable && isVoicemail && !isSelfModifyingOrInternal(packagesModified)) { ContentValues values = new ContentValues(); values.put(VoicemailContract.Voicemails.DIRTY, 1); values.put(VoicemailContract.Voicemails.DELETED, 1); values.put(VoicemailContract.Voicemails.LAST_MODIFIED, getTimeMillis()); - count = mDb.update(table, values, whereClause, whereArgs); + count = qb.update(mDb, values, whereClause, whereArgs); } else { - count = mDb.delete(table, whereClause, whereArgs); + count = qb.delete(mDb, whereClause, whereArgs); } if (count > 0 && isVoicemail) { diff --git a/src/com/android/providers/contacts/MetadataEntryParser.java b/src/com/android/providers/contacts/MetadataEntryParser.java deleted file mode 100644 index 2fe423a8..00000000 --- a/src/com/android/providers/contacts/MetadataEntryParser.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright (C) 2015 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; - -import android.text.TextUtils; -import com.android.providers.contacts.util.NeededForTesting; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; - -@NeededForTesting -public class MetadataEntryParser { - - private final static String UNIQUE_CONTACT_ID = "unique_contact_id"; - private final static String ACCOUNT_TYPE = "account_type"; - private final static String CUSTOM_ACCOUNT_TYPE = "custom_account_type"; - private final static String ENUM_VALUE_FOR_GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT"; - private final static String GOOGLE_ACCOUNT_TYPE = "com.google"; - private final static String ENUM_VALUE_FOR_CUSTOM_ACCOUNT = "CUSTOM_ACCOUNT"; - private final static String ACCOUNT_NAME = "account_name"; - private final static String DATA_SET = "data_set"; - private final static String ENUM_FOR_PLUS_DATA_SET = "GOOGLE_PLUS"; - private final static String ENUM_FOR_CUSTOM_DATA_SET = "CUSTOM"; - private final static String PLUS_DATA_SET_TYPE = "plus"; - private final static String CUSTOM_DATA_SET = "custom_data_set"; - private final static String CONTACT_ID = "contact_id"; - private final static String CONTACT_PREFS = "contact_prefs"; - private final static String SEND_TO_VOICEMAIL = "send_to_voicemail"; - private final static String STARRED = "starred"; - private final static String PINNED = "pinned"; - private final static String AGGREGATION_DATA = "aggregation_data"; - private final static String CONTACT_IDS = "contact_ids"; - private final static String TYPE = "type"; - private final static String FIELD_DATA = "field_data"; - private final static String FIELD_DATA_ID = "field_data_id"; - private final static String FIELD_DATA_PREFS = "field_data_prefs"; - private final static String IS_PRIMARY = "is_primary"; - private final static String IS_SUPER_PRIMARY = "is_super_primary"; - private final static String USAGE_STATS = "usage_stats"; - private final static String USAGE_TYPE = "usage_type"; - private final static String LAST_TIME_USED = "last_time_used"; - private final static String USAGE_COUNT = "usage_count"; - - @NeededForTesting - public static class UsageStats { - @NeededForTesting - final String mUsageType; - @NeededForTesting - final long mLastTimeUsed; - @NeededForTesting - final int mTimesUsed; - - @NeededForTesting - public UsageStats(String usageType, long lastTimeUsed, int timesUsed) { - this.mUsageType = usageType; - this.mLastTimeUsed = lastTimeUsed; - this.mTimesUsed = timesUsed; - } - } - - @NeededForTesting - public static class FieldData { - @NeededForTesting - final String mDataHashId; - @NeededForTesting - final boolean mIsPrimary; - @NeededForTesting - final boolean mIsSuperPrimary; - @NeededForTesting - final ArrayList<UsageStats> mUsageStatsList; - - @NeededForTesting - public FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary, - ArrayList<UsageStats> usageStatsList) { - this.mDataHashId = dataHashId; - this.mIsPrimary = isPrimary; - this.mIsSuperPrimary = isSuperPrimary; - this.mUsageStatsList = usageStatsList; - } - } - - @NeededForTesting - public static class RawContactInfo { - @NeededForTesting - final String mBackupId; - @NeededForTesting - final String mAccountType; - @NeededForTesting - final String mAccountName; - @NeededForTesting - final String mDataSet; - - @NeededForTesting - public RawContactInfo(String backupId, String accountType, String accountName, - String dataSet) { - this.mBackupId = backupId; - this.mAccountType = accountType; - this.mAccountName = accountName; - mDataSet = dataSet; - } - } - - @NeededForTesting - public static class AggregationData { - @NeededForTesting - final RawContactInfo mRawContactInfo1; - @NeededForTesting - final RawContactInfo mRawContactInfo2; - @NeededForTesting - final String mType; - - @NeededForTesting - public AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2, - String type) { - this.mRawContactInfo1 = rawContactInfo1; - this.mRawContactInfo2 = rawContactInfo2; - this.mType = type; - } - } - - @NeededForTesting - public static class MetadataEntry { - @NeededForTesting - final RawContactInfo mRawContactInfo; - @NeededForTesting - final int mSendToVoicemail; - @NeededForTesting - final int mStarred; - @NeededForTesting - final int mPinned; - @NeededForTesting - final ArrayList<FieldData> mFieldDatas; - @NeededForTesting - final ArrayList<AggregationData> mAggregationDatas; - - @NeededForTesting - public MetadataEntry(RawContactInfo rawContactInfo, - int sendToVoicemail, int starred, int pinned, - ArrayList<FieldData> fieldDatas, - ArrayList<AggregationData> aggregationDatas) { - this.mRawContactInfo = rawContactInfo; - this.mSendToVoicemail = sendToVoicemail; - this.mStarred = starred; - this.mPinned = pinned; - this.mFieldDatas = fieldDatas; - this.mAggregationDatas = aggregationDatas; - } - } - - @NeededForTesting - static MetadataEntry parseDataToMetaDataEntry(String inputData) { - if (TextUtils.isEmpty(inputData)) { - throw new IllegalArgumentException("Input cannot be empty."); - } - - try { - final JSONObject root = new JSONObject(inputData); - // Parse to get rawContactId and account info. - final JSONObject uniqueContactJSON = root.getJSONObject(UNIQUE_CONTACT_ID); - final RawContactInfo rawContactInfo = parseUniqueContact(uniqueContactJSON); - - // Parse contactPrefs to get sendToVoicemail, starred, pinned. - final JSONObject contactPrefs = root.getJSONObject(CONTACT_PREFS); - final boolean sendToVoicemail = contactPrefs.has(SEND_TO_VOICEMAIL) - ? contactPrefs.getBoolean(SEND_TO_VOICEMAIL) : false; - final boolean starred = contactPrefs.has(STARRED) - ? contactPrefs.getBoolean(STARRED) : false; - final int pinned = contactPrefs.has(PINNED) ? contactPrefs.getInt(PINNED) : 0; - - // Parse aggregationDatas - final ArrayList<AggregationData> aggregationsList = new ArrayList<AggregationData>(); - if (root.has(AGGREGATION_DATA)) { - final JSONArray aggregationDatas = root.getJSONArray(AGGREGATION_DATA); - - for (int i = 0; i < aggregationDatas.length(); i++) { - final JSONObject aggregationData = aggregationDatas.getJSONObject(i); - final JSONArray contacts = aggregationData.getJSONArray(CONTACT_IDS); - - if (contacts.length() != 2) { - throw new IllegalArgumentException( - "There should be two contacts for each aggregation."); - } - final JSONObject rawContact1 = contacts.getJSONObject(0); - final RawContactInfo aggregationContact1 = parseUniqueContact(rawContact1); - final JSONObject rawContact2 = contacts.getJSONObject(1); - final RawContactInfo aggregationContact2 = parseUniqueContact(rawContact2); - final String type = aggregationData.getString(TYPE); - if (TextUtils.isEmpty(type)) { - throw new IllegalArgumentException("Aggregation type cannot be empty."); - } - - final AggregationData aggregation = new AggregationData( - aggregationContact1, aggregationContact2, type); - aggregationsList.add(aggregation); - } - } - - // Parse fieldDatas - final ArrayList<FieldData> fieldDatasList = new ArrayList<FieldData>(); - if (root.has(FIELD_DATA)) { - final JSONArray fieldDatas = root.getJSONArray(FIELD_DATA); - - for (int i = 0; i < fieldDatas.length(); i++) { - final JSONObject fieldData = fieldDatas.getJSONObject(i); - final String dataHashId = fieldData.getString(FIELD_DATA_ID); - if (TextUtils.isEmpty(dataHashId)) { - throw new IllegalArgumentException("Field data hash id cannot be empty."); - } - final JSONObject fieldDataPrefs = fieldData.getJSONObject(FIELD_DATA_PREFS); - final boolean isPrimary = fieldDataPrefs.getBoolean(IS_PRIMARY); - final boolean isSuperPrimary = fieldDataPrefs.getBoolean(IS_SUPER_PRIMARY); - - final ArrayList<UsageStats> usageStatsList = new ArrayList<UsageStats>(); - if (fieldData.has(USAGE_STATS)) { - final JSONArray usageStats = fieldData.getJSONArray(USAGE_STATS); - for (int j = 0; j < usageStats.length(); j++) { - final JSONObject usageStat = usageStats.getJSONObject(j); - final String usageType = usageStat.getString(USAGE_TYPE); - if (TextUtils.isEmpty(usageType)) { - throw new IllegalArgumentException("Usage type cannot be empty."); - } - final long lastTimeUsed = usageStat.getLong(LAST_TIME_USED); - final int usageCount = usageStat.getInt(USAGE_COUNT); - - final UsageStats usageStatsParsed = new UsageStats( - usageType, lastTimeUsed, usageCount); - usageStatsList.add(usageStatsParsed); - } - } - - final FieldData fieldDataParse = new FieldData(dataHashId, isPrimary, - isSuperPrimary, usageStatsList); - fieldDatasList.add(fieldDataParse); - } - } - final MetadataEntry metaDataEntry = new MetadataEntry(rawContactInfo, - sendToVoicemail ? 1 : 0, starred ? 1 : 0, pinned, - fieldDatasList, aggregationsList); - return metaDataEntry; - } catch (JSONException e) { - throw new IllegalArgumentException("JSON Exception.", e); - } - } - - private static RawContactInfo parseUniqueContact(JSONObject uniqueContactJSON) { - try { - final String backupId = uniqueContactJSON.getString(CONTACT_ID); - final String accountName = uniqueContactJSON.getString(ACCOUNT_NAME); - String accountType = uniqueContactJSON.getString(ACCOUNT_TYPE); - if (ENUM_VALUE_FOR_GOOGLE_ACCOUNT.equals(accountType)) { - accountType = GOOGLE_ACCOUNT_TYPE; - } else if (ENUM_VALUE_FOR_CUSTOM_ACCOUNT.equals(accountType)) { - accountType = uniqueContactJSON.getString(CUSTOM_ACCOUNT_TYPE); - } else { - throw new IllegalArgumentException("Unknown account type."); - } - - String dataSet = null; - switch (uniqueContactJSON.getString(DATA_SET)) { - case ENUM_FOR_PLUS_DATA_SET: - dataSet = PLUS_DATA_SET_TYPE; - break; - case ENUM_FOR_CUSTOM_DATA_SET: - dataSet = uniqueContactJSON.getString(CUSTOM_DATA_SET); - break; - } - if (TextUtils.isEmpty(backupId) || TextUtils.isEmpty(accountType) - || TextUtils.isEmpty(accountName)) { - throw new IllegalArgumentException( - "Contact backup id, account type, account name cannot be empty."); - } - final RawContactInfo rawContactInfo = new RawContactInfo( - backupId, accountType, accountName, dataSet); - return rawContactInfo; - } catch (JSONException e) { - throw new IllegalArgumentException("JSON Exception.", e); - } - } -} diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java index f3b6daf6..d9dc784e 100644 --- a/src/com/android/providers/contacts/ProfileProvider.java +++ b/src/com/android/providers/contacts/ProfileProvider.java @@ -116,8 +116,8 @@ public class ProfileProvider extends AbstractContactsProvider { mDelegate.notifyChange(); } - protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetWork) { - mDelegate.notifyChange(syncToNetwork, syncToMetadataNetWork); + protected void notifyChange(boolean syncToNetwork) { + mDelegate.notifyChange(syncToNetwork); } protected Locale getLocale() { diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java index e421654c..aeaa0e7d 100644 --- a/src/com/android/providers/contacts/SearchIndexManager.java +++ b/src/com/android/providers/contacts/SearchIndexManager.java @@ -55,7 +55,8 @@ public class SearchIndexManager { 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; + private static final String ROW_ID_KEY = "rowid"; + private static final int SEARCH_INDEX_VERSION = 2; private static final class ContactIndexQuery { public static final String[] COLUMNS = { @@ -327,7 +328,7 @@ public class SearchIndexManager { // Remove affected search_index rows. final SQLiteDatabase db = mDbHelper.getWritableDatabase(); final int deleted = db.delete(Tables.SEARCH_INDEX, - SearchIndexColumns.CONTACT_ID + " IN (SELECT " + + ROW_ID_KEY + " IN (SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " WHERE " + rawContactsSelection + @@ -400,6 +401,7 @@ public class SearchIndexManager { mValues.put(SearchIndexColumns.NAME, builder.getName()); mValues.put(SearchIndexColumns.TOKENS, builder.getTokens()); mValues.put(SearchIndexColumns.CONTACT_ID, contactId); + mValues.put(ROW_ID_KEY, contactId); db.insert(Tables.SEARCH_INDEX, null, mValues); } private int getSearchIndexVersion() { diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java index dfb6d696..86dae01b 100644 --- a/src/com/android/providers/contacts/TransactionContext.java +++ b/src/com/android/providers/contacts/TransactionContext.java @@ -35,7 +35,6 @@ public class TransactionContext { /** Map from raw contact id to account Id */ private ArrayMap<Long, Long> mInsertedRawContactsAccounts; private ArraySet<Long> mUpdatedRawContacts; - private ArraySet<Long> mMetadataDirtyRawContacts; private ArraySet<Long> mBackupIdChangedRawContacts; private ArraySet<Long> mDirtyRawContacts; // Set used to track what has been changed and deleted. This is needed so we can update the @@ -78,22 +77,6 @@ public class TransactionContext { markRawContactChangedOrDeletedOrInserted(rawContactId); } - public void markRawContactMetadataDirty(long rawContactId, boolean isMetadataSyncAdapter) { - if (!isMetadataSyncAdapter) { - if (mMetadataDirtyRawContacts == null) { - mMetadataDirtyRawContacts = new ArraySet<>(); - } - mMetadataDirtyRawContacts.add(rawContactId); - } - } - - public void markBackupIdChangedRawContact(long rawContactId) { - if (mBackupIdChangedRawContacts == null) { - mBackupIdChangedRawContacts = new ArraySet<>(); - } - mBackupIdChangedRawContacts.add(rawContactId); - } - public void markRawContactChangedOrDeletedOrInserted(long rawContactId) { if (mChangedRawContacts == null) { mChangedRawContacts = new ArraySet<>(); @@ -131,16 +114,6 @@ public class TransactionContext { return mDirtyRawContacts; } - public Set<Long> getMetadataDirtyRawContactIds() { - if (mMetadataDirtyRawContacts == null) mMetadataDirtyRawContacts = new ArraySet<>(); - return mMetadataDirtyRawContacts; - } - - public Set<Long> getBackupIdChangedRawContacts() { - if (mBackupIdChangedRawContacts == null) mBackupIdChangedRawContacts = new ArraySet<>(); - return mBackupIdChangedRawContacts; - } - public Set<Long> getChangedRawContactIds() { if (mChangedRawContacts == null) mChangedRawContacts = new ArraySet<>(); return mChangedRawContacts; @@ -176,7 +149,6 @@ public class TransactionContext { mUpdatedRawContacts = null; mUpdatedSyncStates = null; mDirtyRawContacts = null; - mMetadataDirtyRawContacts = null; mChangedRawContacts = null; mBackupIdChangedRawContacts = null; } diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java index daecd973..ef7a3758 100644 --- a/src/com/android/providers/contacts/VoicemailContentProvider.java +++ b/src/com/android/providers/contacts/VoicemailContentProvider.java @@ -24,6 +24,7 @@ import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; +import android.content.AttributionSource; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -119,24 +120,23 @@ public class VoicemailContentProvider extends ContentProvider } @Override - protected int enforceReadPermissionInner(Uri uri, String callingPkg, - @Nullable String featureId, IBinder callerToken) throws SecurityException { + protected int enforceReadPermissionInner(Uri uri, + @NonNull AttributionSource attributionSource) throws SecurityException { // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state. if (mVoicemailPermissions.callerHasCarrierPrivileges()) { return MODE_ALLOWED; } - return super.enforceReadPermissionInner(uri, callingPkg, featureId, callerToken); + return super.enforceReadPermissionInner(uri, attributionSource); } - @Override - protected int enforceWritePermissionInner(Uri uri, String callingPkg, - @Nullable String featureId, IBinder callerToken) throws SecurityException { + protected int enforceWritePermissionInner(Uri uri, + @NonNull AttributionSource attributionSource) throws SecurityException { // Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state. if (mVoicemailPermissions.callerHasCarrierPrivileges()) { return MODE_ALLOWED; } - return super.enforceWritePermissionInner(uri, callingPkg, featureId, callerToken); + return super.enforceWritePermissionInner(uri, attributionSource); } @VisibleForTesting diff --git a/src/com/android/providers/contacts/VoicemailPermissions.java b/src/com/android/providers/contacts/VoicemailPermissions.java index 58e7a146..b38f7f58 100644 --- a/src/com/android/providers/contacts/VoicemailPermissions.java +++ b/src/com/android/providers/contacts/VoicemailPermissions.java @@ -151,7 +151,12 @@ public class VoicemailPermissions { } private static boolean packageHasCarrierPrivileges(TelephonyManager tm, String packageName) { - return tm.checkCarrierPrivilegesForPackageAnyPhone(packageName) - == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; + final long token = Binder.clearCallingIdentity(); + try { + return tm.checkCarrierPrivilegesForPackageAnyPhone(packageName) + == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; + } finally { + Binder.restoreCallingIdentity(token); + } } } diff --git a/src/com/android/providers/contacts/util/LogFields.java b/src/com/android/providers/contacts/util/LogFields.java new file mode 100644 index 00000000..4d07ca4b --- /dev/null +++ b/src/com/android/providers/contacts/util/LogFields.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 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.net.Uri; + +public final class LogFields { + + private final int apiType; + + private final int uriType; + + private final boolean callerIsSyncAdapter; + + private final long startNanos; + + private Exception exception; + + private Uri resultUri; + + private int resultCount; + + private int methodCall; + + public LogFields(int apiType, int uriType, boolean callerIsSyncAdapter, long startNanos) { + this.apiType = apiType; + this.uriType = uriType; + this.callerIsSyncAdapter = callerIsSyncAdapter; + this.startNanos = startNanos; + } + + public int getApiType() { + return apiType; + } + + public int getUriType() { + return uriType; + } + + public boolean isCallerIsSyncAdapter() { + return callerIsSyncAdapter; + } + + public long getStartNanos() { + return startNanos; + } + + public Exception getException() { + return exception; + } + + public Uri getResultUri() { + return resultUri; + } + + public int getResultCount() { + return resultCount; + } + + public int getMethodCall() { + return methodCall; + } + + public static final class Builder { + private int apiType; + private int uriType; + private boolean callerIsSyncAdapter; + private long startNanos; + private Exception exception; + private Uri resultUri; + private int resultCount; + private int methodCall; + + private Builder() { + } + + public static Builder aLogFields() { + return new Builder(); + } + + public Builder setApiType(int apiType) { + this.apiType = apiType; + return this; + } + + public Builder setUriType(int uriType) { + this.uriType = uriType; + return this; + } + + public Builder setCallerIsSyncAdapter(boolean callerIsSyncAdapter) { + this.callerIsSyncAdapter = callerIsSyncAdapter; + return this; + } + + public Builder setStartNanos(long startNanos) { + this.startNanos = startNanos; + return this; + } + + public Builder setException(Exception exception) { + this.exception = exception; + return this; + } + + public Builder setResultUri(Uri resultUri) { + this.resultUri = resultUri; + return this; + } + + public Builder setResultCount(int resultCount) { + this.resultCount = resultCount; + return this; + } + + public Builder setMethodCall(int methodCall) { + this.methodCall = methodCall; + return this; + } + + public LogFields build() { + LogFields logFields = new LogFields(apiType, uriType, callerIsSyncAdapter, startNanos); + logFields.resultCount = this.resultCount; + logFields.exception = this.exception; + logFields.resultUri = this.resultUri; + logFields.methodCall = this.methodCall; + return logFields; + } + } +} diff --git a/src/com/android/providers/contacts/util/LogUtils.java b/src/com/android/providers/contacts/util/LogUtils.java new file mode 100644 index 00000000..a564a359 --- /dev/null +++ b/src/com/android/providers/contacts/util/LogUtils.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020 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.os.SystemClock; +import android.util.StatsEvent; +import android.util.StatsLog; + +public class LogUtils { + // Keep in sync with ContactsProviderStatus#ResultType in + // frameworks/proto_logging/stats/atoms.proto file. + public interface ResultType { + int SUCCESS = 1; + int FAIL = 2; + int ILLEGAL_ARGUMENT = 3; + int UNSUPPORTED_OPERATION = 4; + } + + // Keep in sync with ContactsProviderStatus#ApiType in + // frameworks/proto_logging/stats/atoms.proto file. + public interface ApiType { + int QUERY = 1; + int INSERT = 2; + int UPDATE = 3; + int DELETE = 4; + int CALL = 5; + } + + // Keep in sync with ContactsProviderStatus#CallerType in + // frameworks/proto_logging/stats/atoms.proto file. + public interface CallerType { + int CALLER_IS_SYNC_ADAPTER = 1; + int CALLER_IS_NOT_SYNC_ADAPTER = 2; + } + + // Keep in sync with ContactsProviderStatus#MethodCall in + // frameworks/proto_logging/stats/atoms.proto file. + public interface MethodCall { + int ADD_SIM_ACCOUNTS = 1; + int REMOVE_SIM_ACCOUNTS = 2; + int GET_SIM_ACCOUNTS = 3; + } + + private static final int STATSD_LOG_ATOM_ID = 301; + + public static void log(LogFields logFields) { + StatsLog.write(StatsEvent.newBuilder() + .setAtomId(STATSD_LOG_ATOM_ID) + .writeInt(logFields.getApiType()) + .writeInt(logFields.getUriType()) + .writeInt(getCallerType(logFields.isCallerIsSyncAdapter())) + .writeInt(getResultType(logFields.getException())) + .writeInt(logFields.getResultCount()) + .writeLong(getLatencyMicros(logFields.getStartNanos())) + .writeInt(0) // Empty value for TaskType + .writeInt(logFields.getMethodCall()) + .usePooledBuffer() + .build()); + } + + private static int getCallerType(boolean callerIsSyncAdapter) { + return callerIsSyncAdapter + ? CallerType.CALLER_IS_SYNC_ADAPTER : CallerType.CALLER_IS_NOT_SYNC_ADAPTER; + } + + private static int getResultType(Exception exception) { + if (exception == null) { + return ResultType.SUCCESS; + } else if (exception instanceof IllegalArgumentException) { + return ResultType.ILLEGAL_ARGUMENT; + } else if (exception instanceof UnsupportedOperationException) { + return ResultType.UNSUPPORTED_OPERATION; + } else { + return ResultType.FAIL; + } + } + + private static long getLatencyMicros(long startNanos) { + return (SystemClock.elapsedRealtimeNanos() - startNanos) / 1000; + } +} + + |
