am 1317d37c: am bcaff37b: am 27174a97: am 8da96d94: am 07b5ddba: Fix CallLogProviderTest.testAddCall
* commit '1317d37cf02dfcff12f586bf1c84db3a36c5888d':
Fix CallLogProviderTest.testAddCall
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4eb2bdf..8017966 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -59,6 +59,13 @@
android:permission="com.android.voicemail.permission.ADD_VOICEMAIL">
</provider>
+ <provider android:name="ContactMetadataProvider"
+ android:authorities="com.android.contacts.metadata"
+ android:multiprocess="false"
+ android:exported="true"
+ android:permission="android.permission.READ_WRITE_CONTACT_METADATA">
+ </provider>
+
<!-- Handles database upgrades after OTAs, then disables itself -->
<receiver android:name="ContactsUpgradeReceiver">
<!-- This broadcast is sent after the core system has finished
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml
index d11f636..23c0885 100644
--- a/res/values-az-rAZ/strings.xml
+++ b/res/values-az-rAZ/strings.xml
@@ -19,14 +19,11 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Kontakt Yaddaşı"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktlar"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontakt data bazası təkmilləşdirilir."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakt təkmilləşdirməsi əlavə yaddaş tələb edir."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontakt üçün yaddaş təkmilləşdirilir."</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Təkmilləşdirməni tamamlamaq üçün toxunun."</string>
<string name="default_directory" msgid="93961630309570294">"Kontaktlar"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"Digər"</string>
- <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Bütün səsli məktublara giriş"</string>
- <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Tətbiqə bu cihazın giriş əldə edə biləcəyi bütün səsli məktubları saxlamağa və əldə etməyə imkan verir."</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Səsli mesaj göndərən: "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Kontakt data bazasını kopyalayın"</string>
<string name="debug_dump_database_message" msgid="406438635002392290">"Siz 1) informasiyaya və daxili yaddaş ehtiyatındakı zəng jurnalına bağlı data bazanızın nüsxəsini hazırlamaq 2) və onu e-poçt ilə göndərmək üzrəsiniz. Onu cihazdan kənarda və ya alınmış e-məktubda uğurla kopyalayandan sonra nüsxəsini silməyi unutmayın."</string>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
index 7f58e0c..16cd70b 100644
--- a/res/values-gu-rIN/strings.xml
+++ b/res/values-gu-rIN/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core એપ્લિકેશન્સ"</string>
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core એપ્લિકેશનો"</string>
<string name="app_label" msgid="3389954322874982620">"સંપર્કો સ્ટોરેજ"</string>
<string name="provider_label" msgid="6012150850819899907">"સંપર્કો"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"સંપર્કો અપગ્રેડને વધુ મેમરીની જરૂર છે."</string>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
index 5493e4b..a3aa6d1 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -17,10 +17,10 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="sharedUserLabel" msgid="8024311725474286801">"Android കോർ അപ്ലിക്കേഷനുകൾ"</string>
- <string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സംഭരണം"</string>
+ <string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സ്റ്റോറേജ്"</string>
<string name="provider_label" msgid="6012150850819899907">"വിലാസങ്ങൾ"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"കോൺടാക്റ്റുകൾ അപ്ഗ്രേഡുചെയ്യാൻ കൂടുതൽ മെമ്മറി ആവശ്യമാണ്."</string>
- <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"കോൺടാക്റ്റുകൾക്കായുള്ള സംഭരണം അപ്ഗ്രേഡുചെയ്യുന്നു"</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"കോൺടാക്റ്റുകൾക്കായുള്ള സ്റ്റോറേജ് അപ്ഗ്രേഡുചെയ്യുന്നു"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"അപ്ഗ്രേഡ് പൂർത്തിയാക്കാൻ സ്പർശിക്കുക."</string>
<string name="default_directory" msgid="93961630309570294">"വിലാസങ്ങൾ"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"മറ്റുള്ളവ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 6669a21..1cf97fd 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -23,7 +23,7 @@
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Oppgraderer lagring for kontakter"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Trykk for å fullføre oppgraderingen."</string>
<string name="default_directory" msgid="93961630309570294">"Kontakter"</string>
- <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Annet"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Talemelding fra "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Kopiér kontaktdatabasen"</string>
<string name="debug_dump_database_message" msgid="406438635002392290">"Du er i ferd med å 1) lage en kopi av databasen som omfatter all kontaktrelatert informasjon og alle anropslogger til den interne lagringsplassen, og 2) sende kopien med e-post. Husk å slette kopien så snart du har kopiert den fra enheten eller når e-posten er mottatt."</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..6906957
--- /dev/null
+++ b/res/values-pt-rBR/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Principais apps do Android"</string>
+ <string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
+ <string name="provider_label" msgid="6012150850819899907">"Contatos"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contatos precisa de mais memória."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Atualização do armazenamento para contatos"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
+ <string name="default_directory" msgid="93961630309570294">"Contatos"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"Copiar banco de dados de contatos"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de chamadas e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"Excluir agora"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"Iniciar"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Escolha um programa para enviar o arquivo"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"BD de contatos anexado"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"Meu banco de dados de contatos está anexado, Lá estão todas as informações de meus contatos. Use-o com cuidado."</string>
+</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 89d5140..97b65d5 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -23,7 +23,7 @@
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Надограђивање меморије за контакте"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Додирните да бисте довршили надоградњу."</string>
<string name="default_directory" msgid="93961630309570294">"Контакти"</string>
- <string name="local_invisible_directory" msgid="705244318477396120">"Други"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Другo"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Говорна пошта од "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Копирање базе података са контактима"</string>
<string name="debug_dump_database_message" msgid="406438635002392290">"Управо ћете 1) направити копију базе података која садржи све информације у вези са контактима и целокупну евиденцију позива у интерној меморији и 2) послати је имејлом. Не заборавите да избришете копију чим је будете копирали са уређаја или чим будете примили имејл."</string>
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index 530a31b..b7039a2 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -194,7 +194,8 @@
Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
// Announce the change to listeners of the contacts authority
- mContactsProvider.notifyChange(false);
+ mContactsProvider.notifyChange(/* syncToNetwork =*/false,
+ /* syncToMetadataNetwork =*/false);
}
@VisibleForTesting
diff --git a/src/com/android/providers/contacts/ContactMetadataProvider.java b/src/com/android/providers/contacts/ContactMetadataProvider.java
new file mode 100644
index 0000000..eff13fc
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactMetadataProvider.java
@@ -0,0 +1,349 @@
+/*
+ * 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.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.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 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);
+ }
+
+ 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 ContactsDatabaseHelper mDbHelper;
+ private ContactsProvider2 mContactsProvider;
+
+ @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;
+ 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) {
+
+ 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);
+ qb.setTables(Views.METADATA_SYNC);
+ qb.setProjectionMap(sMetadataProjectionMap);
+ qb.setStrict(true);
+
+ final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
+
+ final int match = sURIMatcher.match(uri);
+ switch (match) {
+ case METADATA_SYNC:
+ break;
+
+ case METADATA_SYNC_ID: {
+ selectionBuilder.addClause(getEqualityClause(MetadataSync._ID,
+ ContentUris.parseId(uri)));
+ 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;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ // Insert the new entry, and also parse the data column to update related tables.
+ final long metadataSyncId = updateOrInsertDataToMetadataSync(
+ db, uri, values, /* isInsert = */ true);
+ db.setTransactionSuccessful();
+ return ContentUris.withAppendedId(uri, metadataSyncId);
+ } finally {
+ db.endTransaction();
+ }
+}
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ 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;
+ 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) {
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ // Update the metadata entry and parse the data column to update related tables.
+ updateOrInsertDataToMetadataSync(db, uri, values, /* isInsert = */ false);
+ db.setTransactionSuccessful();
+ return 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "applyBatch: " + operations.size() + " ops");
+ }
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentProviderResult[] results = super.applyBatch(operations);
+ db.setTransactionSuccessful();
+ return results;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "bulkInsert: " + values.length + " inserts");
+ }
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ final int numValues = super.bulkInsert(uri, values);
+ db.setTransactionSuccessful();
+ return numValues;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Insert or update a non-deleted entry to MetadataSync table, and also parse the data column
+ * to update related tables for the raw contact.
+ * Set 'isInsert' as true if it's insert and false if update.
+ * Returns new inserted metadataSyncId if it's insert, and returns 1 if it's update.
+ */
+ private long updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values,
+ boolean isInsert) {
+ final int matchUri = sURIMatcher.match(uri);
+ if ((isInsert && matchUri != METADATA_SYNC) ||
+ (!isInsert && matchUri != METADATA_SYNC && matchUri != METADATA_SYNC_ID)) {
+ 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));
+ }
+
+ long result = 0;
+ if (matchUri == METADATA_SYNC_ID) {
+ // Update for the metadataSyncId.
+ final long metadataSyncId = ContentUris.parseId(uri);
+ final String selection = MetadataSync._ID + "=?";
+ final String[] selectionArgs = new String[1];
+ selectionArgs[0] = String.valueOf(metadataSyncId);
+ db.update(Tables.METADATA_SYNC, values, selection, selectionArgs);
+ result = 1;
+ } else {
+ // Update or insert for backupId and account info.
+ final Long accountId = replaceAccountInfoByAccountId(uri, values);
+ final String rawContactBackupId = values.getAsString(MetadataSync.RAW_CONTACT_BACKUP_ID);
+ deleted = 0; //Only insert or update non-deleted metadata
+ if (accountId == null || rawContactBackupId == null) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "Invalid identifier is found: accountId=" + accountId + "; " +
+ "rawContactBackupId=" + rawContactBackupId, uri));
+ }
+
+ if (isInsert) {
+ result = mDbHelper.insertMetadataSync(rawContactBackupId, accountId, data, deleted);
+ if (result <= 0) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "Metadata insertion failed. Values= " + values.toString(), uri));
+ }
+ } else {
+ mDbHelper.updateMetadataSync(rawContactBackupId, accountId, data, deleted);
+ result = 1;
+ }
+ }
+
+ // 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 result;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index aac37ba..bcf0b75 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -63,6 +63,7 @@
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.PhotoFiles;
import android.provider.ContactsContract.PinnedPositions;
@@ -79,6 +80,7 @@
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
+import android.util.Base64;
import android.util.Log;
import com.android.common.content.SyncStateContentProviderHelper;
@@ -92,6 +94,9 @@
import libcore.icu.ICU;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -119,9 +124,10 @@
* 800-899 Kitkat
* 900-999 Lollipop
* 1000-1099 M
+ * 1100-1199 N
* </pre>
*/
- static final int DATABASE_VERSION = 1011;
+ static final int DATABASE_VERSION = 1104;
public interface Tables {
public static final String CONTACTS = "contacts";
@@ -150,6 +156,7 @@
public static final String DEFAULT_DIRECTORY = "default_directory";
public static final String SEARCH_INDEX = "search_index";
public static final String VOICEMAIL_STATUS = "voicemail_status";
+ public static final String METADATA_SYNC = "metadata_sync";
public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris";
// This list of tables contains auto-incremented sequences.
@@ -273,6 +280,15 @@
+ " 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 {
@@ -304,6 +320,7 @@
public static final String GROUPS = "view_groups";
public static final String DATA_USAGE_STAT = "view_data_usage_stat";
public static final String STREAM_ITEMS = "view_stream_items";
+ public static final String METADATA_SYNC = "view_metadata_sync";
}
public interface Projections {
@@ -441,6 +458,8 @@
public static final String CONCRETE_PINNED =
Tables.RAW_CONTACTS + "." + RawContacts.PINNED;
+ public static final String CONCRETE_METADATA_DIRTY =
+ Tables.RAW_CONTACTS + "." + RawContacts.METADATA_DIRTY;
public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
public static final String AGGREGATION_NEEDED = "aggregation_needed";
@@ -729,6 +748,16 @@
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;
+ }
+
private interface EmailQuery {
public static final String TABLE = Tables.DATA;
@@ -984,9 +1013,20 @@
private SQLiteStatement mStatusUpdateDelete;
private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
private SQLiteStatement mContactInDefaultDirectoryQuery;
+ private SQLiteStatement mMetadataSyncInsert;
+ private SQLiteStatement mMetadataSyncUpdate;
private StringBuilder mSb = new StringBuilder();
+ private MessageDigest mMessageDigest;
+ {
+ try {
+ mMessageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("No such algorithm.", e);
+ }
+ }
+
private boolean mUseStrictPhoneNumberComparison;
private String[] mSelectionArgs1 = new String[1];
@@ -1217,6 +1257,7 @@
RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.METADATA_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
RawContacts.AGGREGATION_MODE_DEFAULT + "," +
@@ -1581,6 +1622,19 @@
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, " +
@@ -1830,6 +1884,7 @@
db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
+ db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
String dataColumns =
Data.IS_PRIMARY + ", "
@@ -1982,6 +2037,7 @@
+ RawContacts.AGGREGATION_MODE + ", "
+ RawContacts.RAW_CONTACT_IS_READ_ONLY + ", "
+ RawContacts.DELETED + ", "
+ + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
+ RawContacts.DISPLAY_NAME_SOURCE + ", "
+ RawContacts.DISPLAY_NAME_PRIMARY + ", "
+ RawContacts.DISPLAY_NAME_ALTERNATIVE + ", "
@@ -2035,6 +2091,7 @@
String rawEntitiesSelect = "SELECT "
+ RawContacts.CONTACT_ID + ", "
+ RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+ + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
+ dataColumns + ", "
+ syncColumns + ", "
+ Data.SYNC1 + ", "
@@ -2068,6 +2125,7 @@
+ RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", "
+ RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+ RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+ + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
+ dataColumns + ", "
+ syncColumns + ", "
+ contactsColumns + ", "
@@ -2157,6 +2215,21 @@
+ 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);
}
private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) {
@@ -2895,19 +2968,33 @@
oldVersion = 1009;
}
- if (oldVersion < 1010) {
- upgradeToVersion1010(db);
- rebuildSqliteStats = true;
- oldVersion = 1010;
+ if (oldVersion < 1100) {
+ upgradeToVersion1100(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1100;
}
- if (oldVersion < 1011) {
- // There was a merge error, so do step1010 again, and also re-crete views.
- // upgradeToVersion1010() is safe to re-run.
- upgradeToVersion1010(db);
- rebuildSqliteStats = true;
+ if (oldVersion < 1101) {
+ upgradeToVersion1101(db);
+ oldVersion = 1101;
+ }
+
+ if (oldVersion < 1102) {
+ // Version 1009 was added *after* 1100/1101. For master devices
+ // that have already been updated to 1101, we do it again.
+ upgradeToVersion1009(db);
+ oldVersion = 1102;
+ }
+
+ if (oldVersion < 1103) {
upgradeViewsAndTriggers = true;
- oldVersion = 1011;
+ oldVersion = 1103;
+ }
+
+ if (oldVersion < 1104) {
+ upgradeToVersion1104(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1104;
}
if (upgradeViewsAndTriggers) {
@@ -3646,7 +3733,7 @@
private void upgradeToVersion401(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
Contacts._ID + " INTEGER PRIMARY KEY" +
- ");");
+ ");");
db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
" SELECT " + Contacts._ID +
" FROM " + Tables.CONTACTS +
@@ -3801,8 +3888,8 @@
" ADD " + Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0");
db.execSQL(
"UPDATE " + Tables.GROUPS +
- " SET " + Groups.GROUP_IS_READ_ONLY + "=1" +
- " WHERE " + Groups.SYSTEM_ID + " NOT NULL");
+ " SET " + Groups.GROUP_IS_READ_ONLY + "=1" +
+ " WHERE " + Groups.SYSTEM_ID + " NOT NULL");
}
private void upgradeToVersion416(SQLiteDatabase db) {
@@ -4419,14 +4506,97 @@
// Log verbose because this should be the majority case.
Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
}
- }
+ }
+
public void upgradeToVersion1009(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ try {
+ db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ } catch (SQLiteException ignore) {
+ }
}
- public void upgradeToVersion1010(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS metadata_sync");
+ private void upgradeToVersion1100(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE raw_contacts ADD metadata_dirty INTEGER NOT NULL DEFAULT 0;");
+ }
+
+ // Data.hash_id column is used for metadata backup, and this upgrade is to generate
+ // hash_id column. Usually data1 and data2 are two main columns to store data info.
+ // But for photo, we don't use data1 and data2, instead, use data15 to store photo blob.
+ // So this upgrade generates hash_id from (data1 + data2) or (data15) using sha-1.
+ private void upgradeToVersion1101(SQLiteDatabase db) {
+ final SQLiteStatement update = db.compileStatement(
+ "UPDATE " + Tables.DATA +
+ " SET " + Data.HASH_ID + "=?" +
+ " WHERE " + Data._ID + "=?"
+ );
+ final String selection = Data.HASH_ID + " IS NULL";
+ final Cursor c = db.query(Tables.DATA, new String[] {Data._ID, Data.DATA1, Data.DATA2,
+ Data.DATA15},
+ selection, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ final long dataId = c.getLong(0);
+ final String data1 = c.getString(1);
+ final String data2 = c.getString(2);
+ final byte[] data15 = c.getBlob(3);
+ final String hashId = generateHashId(data1, data2, data15);
+ if (!TextUtils.isEmpty(hashId)) {
+ update.bindString(1, hashId);
+ update.bindLong(2, dataId);
+ update.execute();
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * 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.
+ */
+ 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);");
+ }
+
+ /**
+ * Generate hash_id from data1, data2 and data15 columns.
+ * If one of data1 and data2 is not null, using data1 and data2 to get hash_id,
+ * otherwise, using data15 to generate.
+ */
+ public String generateHashId(String data1, String data2, byte[] data15) {
+ final StringBuilder sb = new StringBuilder();
+ byte[] hashInput = null;
+ if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
+ sb.append(data1);
+ sb.append(data2);
+ hashInput = sb.toString().getBytes();
+ } else if (data15 != null) {
+ hashInput = data15;
+ }
+ if (hashInput != null) {
+ final String hashId = generateHashIdForData(hashInput);
+ return hashId;
+ } else {
+ return null;
+ }
+ }
+
+ // Use SHA-1 hash method to generate hash string for the input.
+ @VisibleForTesting
+ String generateHashIdForData(byte[] input) {
+ synchronized (mMessageDigest) {
+ final byte[] hashResult = mMessageDigest.digest(input);
+ return Base64.encodeToString(hashResult, Base64.DEFAULT);
+ }
}
public String extractHandleFromEmailAddress(String email) {
@@ -4617,6 +4787,9 @@
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");
@@ -4952,14 +5125,14 @@
}
final SQLiteStatement select = getWritableDatabase().compileStatement(
"SELECT " + AccountsColumns._ID +
- " FROM " + Tables.ACCOUNTS +
- " WHERE " +
- "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
- "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
- "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
- "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
- "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
- "(" + AccountsColumns.DATA_SET + "=?3))");
+ " FROM " + Tables.ACCOUNTS +
+ " WHERE " +
+ "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
+ "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
+ "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
+ "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
+ "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
+ "(" + AccountsColumns.DATA_SET + "=?3))");
try {
DatabaseUtils.bindObjectToProgram(select, 1, accountWithDataSet.getAccountName());
DatabaseUtils.bindObjectToProgram(select, 2, accountWithDataSet.getAccountType());
@@ -5987,4 +6160,40 @@
" WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
new String[] {String.valueOf(contactId)});
}
+
+ public long insertMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
+ if (mMetadataSyncInsert == null) {
+ mMetadataSyncInsert = getWritableDatabase().compileStatement(
+ "INSERT INTO " + Tables.METADATA_SYNC + "("
+ + MetadataSync.RAW_CONTACT_BACKUP_ID + ", "
+ + MetadataSyncColumns.ACCOUNT_ID + ", "
+ + MetadataSync.DATA + ","
+ + MetadataSync.DELETED + ")" +
+ " VALUES (?,?,?,?)");
+ }
+ mMetadataSyncInsert.bindString(1, backupId);
+ mMetadataSyncInsert.bindLong(2, accountId);
+ data = (data == null) ? "" : data;
+ mMetadataSyncInsert.bindString(3, data);
+ mMetadataSyncInsert.bindLong(4, deleted);
+ return mMetadataSyncInsert.executeInsert();
+ }
+
+ public void updateMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
+ if (mMetadataSyncUpdate == null) {
+ mMetadataSyncUpdate = getWritableDatabase().compileStatement(
+ "UPDATE " + Tables.METADATA_SYNC
+ + " SET " + MetadataSync.DATA + "=?,"
+ + MetadataSync.DELETED + "=?"
+ + " WHERE " + MetadataSync.RAW_CONTACT_BACKUP_ID + "=? AND "
+ + MetadataSyncColumns.ACCOUNT_ID + "=?");
+ }
+
+ data = (data == null) ? "" : data;
+ mMetadataSyncUpdate.bindString(1, data);
+ mMetadataSyncUpdate.bindLong(2, deleted);
+ mMetadataSyncUpdate.bindString(3, backupId);
+ mMetadataSyncUpdate.bindLong(4, accountId);
+ mMetadataSyncUpdate.execute();
+ }
}
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 7dc7e7e..abe54e9 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -92,6 +92,7 @@
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;
@@ -112,6 +113,7 @@
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.Base64;
import android.util.Log;
import com.android.common.content.ProjectionMap;
import com.android.common.content.SyncStateContentProviderHelper;
@@ -129,6 +131,7 @@
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.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
@@ -155,6 +158,11 @@
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.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.util.Clock;
import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.DbQueryUtils;
@@ -178,7 +186,10 @@
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
import java.io.Writer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -538,6 +549,23 @@
" SET " + RawContacts.DIRTY + "=1" +
" WHERE " + RawContacts._ID + " IN (";
+ /** Sql for updating METADATA_DIRTY flag on multiple raw contacts */
+ private static final String UPDATE_RAW_CONTACT_SET_METADATA_DIRTY_SQL =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.METADATA_DIRTY + "=1" +
+ " WHERE " + RawContacts._ID + " IN (";
+
+ // Sql for updating MetadataSync.DELETED flag on multiple raw contacts.
+ // When using this sql, add comma separated raw contacts ids and "))".
+ private static final String UPDATE_METADATASYNC_SET_DELETED_SQL =
+ "UPDATE " + Tables.METADATA_SYNC
+ + " SET " + MetadataSync.DELETED + "=1"
+ + " WHERE " + MetadataSync._ID + " IN "
+ + "(SELECT " + MetadataSyncColumns.CONCRETE_ID
+ + " FROM " + Tables.RAW_CONTACTS_JOIN_METADATA_SYNC
+ + " WHERE " + RawContactsColumns.CONCRETE_DELETED + "=1 AND "
+ + RawContactsColumns.CONCRETE_ID + " IN (";
+
/** Sql for updating VERSION on multiple raw contacts */
private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
"UPDATE " + Tables.RAW_CONTACTS +
@@ -844,6 +872,7 @@
.add(RawContacts.PINNED)
.add(RawContacts.AGGREGATION_MODE)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
+ .add(RawContacts.METADATA_DIRTY)
.addAll(sRawContactColumns)
.addAll(sRawContactSyncColumns)
.build();
@@ -914,6 +943,7 @@
.add(Data._ID, "MIN(" + Data._ID + ")")
.add(RawContacts.CONTACT_ID)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
+ .add(Data.HASH_ID)
.addAll(sDataColumns)
.addAll(sDataPresenceColumns)
.addAll(sContactsColumns)
@@ -1443,6 +1473,7 @@
private boolean mVisibleTouched = false;
private boolean mSyncToNetwork;
+ private boolean mSyncToMetadataNetWork;
private LocaleSet mCurrentLocales;
private int mContactsAccountCount;
@@ -1459,6 +1490,9 @@
private int mFastScrollingIndexCacheMissCount;
private long mTotalTimeFastScrollingIndexGenerate;
+ // MetadataSync flag.
+ private boolean mMetadataSyncEnabled;
+
@Override
public boolean onCreate() {
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -1494,6 +1528,9 @@
mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext());
+ mMetadataSyncEnabled = android.provider.Settings.Global.getInt(
+ getContext().getContentResolver(), Global.CONTACT_METADATA_SYNC, 0) == 1;
+
mContactsHelper = getDatabaseHelper(getContext());
mDbHelper.set(mContactsHelper);
@@ -1977,7 +2014,8 @@
ContentValues updateValues = new ContentValues();
updateValues.putNull(Photo.PHOTO_FILE_ID);
updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
- updateValues, null, null, false);
+ updateValues, null, null, /* callerIsSyncAdapter =*/false,
+ /* callerIsMetadataSyncAdapter =*/false);
}
if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) {
// For missing photos that were in stream item photos, just delete the
@@ -1998,7 +2036,7 @@
}
@Override
- protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
return ContactsDatabaseHelper.getInstance(context);
}
@@ -2419,9 +2457,12 @@
for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) {
mDbHelper.get().updateRawContactDisplayName(db, rawContactId);
mAggregator.get().onRawContactInsert(mTransactionContext.get(), db, rawContactId);
+ if (mMetadataSyncEnabled) {
+ updateMetadataOnRawContactInsert(db, rawContactId);
+ }
}
- Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
+ final Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
if (!dirtyRawContacts.isEmpty()) {
mSb.setLength(0);
mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
@@ -2430,7 +2471,7 @@
db.execSQL(mSb.toString());
}
- Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
+ final Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
if (!updatedRawContacts.isEmpty()) {
mSb.setLength(0);
mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
@@ -2439,8 +2480,29 @@
db.execSQL(mSb.toString());
}
+ final Set<Long> metadataDirtyRawContacts =
+ mTransactionContext.get().getMetadataDirtyRawContactIds();
+ if (!metadataDirtyRawContacts.isEmpty() && mMetadataSyncEnabled) {
+ mSb.setLength(0);
+ mSb.append(UPDATE_RAW_CONTACT_SET_METADATA_DIRTY_SQL);
+ appendIds(mSb, metadataDirtyRawContacts);
+ mSb.append(")");
+ db.execSQL(mSb.toString());
+ mSyncToMetadataNetWork = true;
+ }
+
final Set<Long> changedRawContacts = mTransactionContext.get().getChangedRawContactIds();
ContactsTableUtil.updateContactLastUpdateByRawContactId(db, changedRawContacts);
+ if (!changedRawContacts.isEmpty() && mMetadataSyncEnabled) {
+ // For the deleted raw contact, set related metadata as deleted
+ // if metadata flag is enabled.
+ mSb.setLength(0);
+ mSb.append(UPDATE_METADATASYNC_SET_DELETED_SQL);
+ appendIds(mSb, changedRawContacts);
+ mSb.append("))");
+ db.execSQL(mSb.toString());
+ mSyncToMetadataNetWork = true;
+ }
// Update sync states.
for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
@@ -2454,6 +2516,54 @@
mTransactionContext.get().clearExceptSearchIndexUpdates();
}
+ @VisibleForTesting
+ void setMetadataSyncForTest(boolean enabled) {
+ mMetadataSyncEnabled = enabled;
+ }
+
+ interface MetadataSyncQuery {
+ String TABLE = Tables.RAW_CONTACTS_JOIN_METADATA_SYNC;
+ String[] COLUMNS = new String[] {
+ MetadataSyncColumns.CONCRETE_ID,
+ MetadataSync.DATA
+ };
+ int METADATA_SYNC_ID = 0;
+ int METADATA_SYNC_DATA = 1;
+ String SELECTION = MetadataSyncColumns.CONCRETE_DELETED + "=0 AND " +
+ RawContactsColumns.CONCRETE_ID + "=?";
+ }
+
+ /**
+ * Fetch the related metadataSync data column for the raw contact id.
+ * Returns null if there's no metadata for the raw contact.
+ */
+ private String queryMetadataSyncData(SQLiteDatabase db, long rawContactId) {
+ String metadataSyncData = null;
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ final Cursor cursor = db.query(MetadataSyncQuery.TABLE,
+ MetadataSyncQuery.COLUMNS, MetadataSyncQuery.SELECTION,
+ mSelectionArgs1, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ metadataSyncData = cursor.getString(MetadataSyncQuery.METADATA_SYNC_DATA);
+ }
+ } finally {
+ cursor.close();
+ }
+ return metadataSyncData;
+ }
+
+ private void updateMetadataOnRawContactInsert(SQLiteDatabase db, long rawContactId) {
+ // Read metadata from MetadataSync table for the raw contact, and update.
+ final String metadataSyncData = queryMetadataSyncData(db, rawContactId);
+ if (TextUtils.isEmpty(metadataSyncData)) {
+ return;
+ }
+ final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(
+ metadataSyncData);
+ updateFromMetaDataEntry(db, metadataEntry);
+ }
+
/**
* Appends comma separated IDs.
* @param ids Should not be empty
@@ -2468,13 +2578,17 @@
@Override
protected void notifyChange() {
- notifyChange(mSyncToNetwork);
+ notifyChange(mSyncToNetwork, mMetadataSyncEnabled);
mSyncToNetwork = false;
+ mSyncToMetadataNetWork = false;
}
- protected void notifyChange(boolean syncToNetwork) {
+ protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) {
getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
syncToNetwork);
+
+ getContext().getContentResolver().notifyChange(MetadataSync.METADATA_AUTHORITY_URI,
+ null, syncToMetadataNetwork);
}
protected void setProviderStatus(int status) {
@@ -2739,6 +2853,7 @@
values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
}
+ final boolean needToUpdateMetadata = shouldMarkMetadataDirtyForRawContact(values);
// Databases that were created prior to the 906 upgrade have a default of Int.MAX_VALUE
// for RawContacts.PINNED. Manually set the value to the correct default (0) if it is not
// set.
@@ -2750,6 +2865,11 @@
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
final long rawContactId = db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, values);
+ if (needToUpdateMetadata) {
+ mTransactionContext.get().markRawContactMetadataDirty(rawContactId,
+ /* isMetadataSyncAdapter =*/false);
+ }
+
final int aggregationMode = getIntValue(values, RawContacts.AGGREGATION_MODE,
RawContacts.AGGREGATION_MODE_DEFAULT);
mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
@@ -2812,6 +2932,9 @@
mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+ // Generate hash_id from data1 and data2 column, since group data stores in data1 field.
+ getDataRowHandler(GroupMembership.CONTENT_ITEM_TYPE).handleHashIdForInsert(
+ groupMembershipValues);
db.insert(Tables.DATA, null, groupMembershipValues);
}
@@ -3830,7 +3953,8 @@
values.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
values.putNull(RawContacts.CONTACT_ID);
values.put(RawContacts.DIRTY, 1);
- return updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+ return updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+ /* callerIsMetadataSyncAdapter =*/false);
}
private int deleteDataUsage() {
@@ -3932,7 +4056,8 @@
String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
+ (selection == null ? "" : " AND " + selection);
- count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
+ count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter,
+ /* callerIsMetadataSyncAdapter =*/false);
break;
}
@@ -3940,7 +4065,8 @@
case PROFILE_DATA: {
invalidateFastScrollingIndexCache();
count = updateData(uri, values, appendAccountToSelection(uri, selection),
- selectionArgs, callerIsSyncAdapter);
+ selectionArgs, callerIsSyncAdapter,
+ /* callerIsMetadataSyncAdapter =*/false);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
@@ -3953,7 +4079,8 @@
case CALLABLES_ID:
case POSTALS_ID: {
invalidateFastScrollingIndexCache();
- count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+ count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter,
+ /* callerIsMetadataSyncAdapter =*/false);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
@@ -4006,7 +4133,8 @@
}
case AGGREGATION_EXCEPTIONS: {
- count = updateAggregationException(db, values);
+ count = updateAggregationException(db, values,
+ /* callerIsMetadataSyncAdapter =*/false);
invalidateFastScrollingIndexCache();
break;
}
@@ -4317,7 +4445,8 @@
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
- updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+ updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+ /* callerIsMetadataSyncAdapter =*/false);
count++;
}
} finally {
@@ -4328,7 +4457,7 @@
}
private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
- boolean callerIsSyncAdapter) {
+ boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
mSelectionArgs1[0] = Long.toString(rawContactId);
@@ -4403,6 +4532,10 @@
if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
aggregator.markForAggregation(rawContactId, aggregationMode, false);
}
+ if (shouldMarkMetadataDirtyForRawContact(values)) {
+ mTransactionContext.get().markRawContactMetadataDirty(
+ rawContactId, callerIsMetadataSyncAdapter);
+ }
if (flagExists(values, RawContacts.STARRED)) {
if (!callerIsSyncAdapter) {
updateFavoritesMembership(rawContactId, flagIsSet(values, RawContacts.STARRED));
@@ -4444,7 +4577,8 @@
}
private int updateData(Uri uri, ContentValues inputValues, String selection,
- String[] selectionArgs, boolean callerIsSyncAdapter) {
+ String[] selectionArgs, boolean callerIsSyncAdapter,
+ boolean callerIsMetadataSyncAdapter) {
final ContentValues values = new ContentValues(inputValues);
values.remove(Data._ID);
@@ -4470,7 +4604,7 @@
selection, selectionArgs, null, -1 /* directory ID */, null);
try {
while(c.moveToNext()) {
- count += updateData(values, c, callerIsSyncAdapter);
+ count += updateData(values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
}
} finally {
c.close();
@@ -4486,7 +4620,8 @@
}
}
- private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
+ private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter,
+ boolean callerIsMetadataSyncAdapter) {
if (values.size() == 0) {
return 0;
}
@@ -4501,7 +4636,7 @@
DataRowHandler rowHandler = getDataRowHandler(mimeType);
boolean updated =
rowHandler.update(db, mTransactionContext.get(), values, c,
- callerIsSyncAdapter);
+ callerIsSyncAdapter, callerIsMetadataSyncAdapter);
if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
}
@@ -4556,11 +4691,17 @@
return 0; // Nothing to update, bail out.
}
- boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
+ final boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
+ final boolean hasPinnedValue = flagExists(values, RawContacts.PINNED);
+ final boolean hasVoiceMailValue = flagExists(values, RawContacts.SEND_TO_VOICEMAIL);
if (hasStarredValue) {
// Mark dirty when changing starred to trigger sync.
values.put(RawContacts.DIRTY, 1);
}
+ if (hasStarredValue || hasPinnedValue || hasVoiceMailValue) {
+ // Mark dirty to trigger metadata syncing.
+ values.put(RawContacts.METADATA_DIRTY, 1);
+ }
mSelectionArgs1[0] = String.valueOf(contactId);
db.update(Tables.RAW_CONTACTS, values, RawContacts.CONTACT_ID + "=?"
@@ -4617,7 +4758,8 @@
return rslt;
}
- private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
+ private int updateAggregationException(SQLiteDatabase db, ContentValues values,
+ boolean callerIsMetadataSyncAdapter) {
Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1);
Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2);
@@ -4656,17 +4798,185 @@
aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId1);
aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId2);
+ mTransactionContext.get().markRawContactMetadataDirty(rawContactId1,
+ callerIsMetadataSyncAdapter);
+ mTransactionContext.get().markRawContactMetadataDirty(rawContactId2,
+ callerIsMetadataSyncAdapter);
// The return value is fake - we just confirm that we made a change, not count actual
// rows changed.
return 1;
}
+ private boolean shouldMarkMetadataDirtyForRawContact(ContentValues values) {
+ return (flagExists(values, RawContacts.STARRED) || flagExists(values, RawContacts.PINNED)
+ || flagExists(values, RawContacts.SEND_TO_VOICEMAIL));
+ }
+
@Override
public void onAccountsUpdated(Account[] accounts) {
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
}
+ interface RawContactsBackupQuery {
+ String TABLE = Tables.RAW_CONTACTS;
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ };
+ int RAW_CONTACT_ID = 0;
+ String SELECTION = RawContacts.DELETED + "=0 AND " +
+ RawContacts.BACKUP_ID + "=? AND " +
+ RawContactsColumns.ACCOUNT_ID + "=?";
+ }
+
+ /**
+ * Fetch rawContactId related to the given backupId.
+ * Return 0 if there's no such rawContact or it's deleted.
+ */
+ private long queryRawContactId(SQLiteDatabase db, String backupId, long accountId) {
+ if (TextUtils.isEmpty(backupId)) {
+ return 0;
+ }
+ mSelectionArgs2[0] = backupId;
+ mSelectionArgs2[1] = String.valueOf(accountId);
+ long rawContactId = 0;
+ final Cursor cursor = db.query(RawContactsBackupQuery.TABLE,
+ RawContactsBackupQuery.COLUMNS, RawContactsBackupQuery.SELECTION,
+ mSelectionArgs2, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ rawContactId = cursor.getLong(RawContactsBackupQuery.RAW_CONTACT_ID);
+ }
+ } finally {
+ cursor.close();
+ }
+ return rawContactId;
+ }
+
+ interface DataHashQuery {
+ String TABLE = Tables.DATA;
+ String[] COLUMNS = new String[] {
+ Data._ID,
+ };
+ int DATA_ID = 0;
+ String SELECTION = Data.RAW_CONTACT_ID + "=? AND " + Data.HASH_ID + "=?";
+ }
+
+ /**
+ * Fetch a list of dataId related to the given hashId.
+ * Return empty list if there's no such data.
+ */
+ private ArrayList<Long> queryDataId(SQLiteDatabase db, long rawContactId, String hashId) {
+ if (rawContactId == 0 || TextUtils.isEmpty(hashId)) {
+ return new ArrayList<>();
+ }
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = hashId;
+ ArrayList<Long> result = new ArrayList<>();
+ long dataId = 0;
+ final Cursor c = db.query(DataHashQuery.TABLE, DataHashQuery.COLUMNS,
+ DataHashQuery.SELECTION, mSelectionArgs2, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ dataId = c.getLong(DataHashQuery.DATA_ID);
+ result.add(dataId);
+ }
+ } finally {
+ c.close();
+ }
+ 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;
+ }
+
+ /**
+ * 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 UsageStats.
+ for (int j = 0; j < fieldData.mUsageStatsList.size(); j++) {
+ final UsageStats usageStats = fieldData.mUsageStatsList.get(j);
+ final String usageType = usageStats.mUsageType;
+ final int typeInt = getDataUsageFeedbackType(usageType.toLowerCase(), null);
+ final long lastTimeUsed = usageStats.mLastTimeUsed;
+ final int timesUsed = usageStats.mTimesUsed;
+ ContentValues usageStatsValues = new ContentValues();
+ usageStatsValues.put(DataUsageStatColumns.DATA_ID, dataId);
+ usageStatsValues.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
+ usageStatsValues.put(DataUsageStatColumns.LAST_TIME_USED, lastTimeUsed);
+ usageStatsValues.put(DataUsageStatColumns.TIMES_USED, timesUsed);
+ updateDataUsageStats(db, usageStatsValues);
+ }
+ }
+ }
+
+ // Update AggregationException table.
+ 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);
+ }
+ }
+
/** return serialized version of {@code accounts} */
@VisibleForTesting
static String accountsToString(Set<Account> accounts) {
@@ -7891,7 +8201,7 @@
* @return A string containing a non-negative integer, or <code>null</code> if
* the parameter is not set, or is set to an invalid value.
*/
- private String getLimit(Uri uri) {
+ static String getLimit(Uri uri) {
String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
if (limitParam == null) {
return null;
@@ -8994,13 +9304,28 @@
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+ final Set<Long> rawContactIds = new HashSet<>();
+ final Cursor cursor = db.rawQuery(rawContactIdSelect.toString(), null);
+ try {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ final long rid = cursor.getLong(0);
+ mTransactionContext.get().markRawContactMetadataDirty(rid,
+ /* isMetadataSyncAdapter =*/false);
+ rawContactIds.add(rid);
+ }
+ } finally {
+ cursor.close();
+ }
+
mSelectionArgs1[0] = String.valueOf(currentTimeMillis);
+ final String rids = TextUtils.join(",", rawContactIds);
db.execSQL("UPDATE " + Tables.RAW_CONTACTS +
" SET " + RawContacts.LAST_TIME_CONTACTED + "=?" +
"," + RawContacts.TIMES_CONTACTED + "=" +
"ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" +
- " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + ")"
+ " WHERE " + RawContacts._ID + " IN (" + rids + ")"
, mSelectionArgs1);
db.execSQL("UPDATE " + Tables.CONTACTS +
" SET " + Contacts.LAST_TIME_CONTACTED + "=?1" +
@@ -9009,7 +9334,7 @@
"," + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=?1" +
" WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID +
" FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + "))"
+ " WHERE " + RawContacts._ID + " IN (" + rids + "))"
, mSelectionArgs1);
return successful;
@@ -9079,6 +9404,52 @@
}
/**
+ * Update {@link Tables#DATA_USAGE_STAT}.
+ * Update or insert usageType, lastTimeUsed, and timesUsed for specific dataId.
+ */
+ private void updateDataUsageStats(SQLiteDatabase db, ContentValues values) {
+ final String dataId = values.getAsString(DataUsageStatColumns.DATA_ID);
+ final String type = values.getAsString(DataUsageStatColumns.USAGE_TYPE_INT);
+ final String lastTimeUsed = values.getAsString(DataUsageStatColumns.LAST_TIME_USED);
+ final String timesUsed = values.getAsString(DataUsageStatColumns.TIMES_USED);
+
+ mSelectionArgs2[0] = dataId;
+ mSelectionArgs2[1] = type;
+ final Cursor cursor = db.query(DataUsageStatQuery.TABLE,
+ DataUsageStatQuery.COLUMNS, DataUsageStatQuery.SELECTION,
+ mSelectionArgs2, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ final long id = cursor.getLong(DataUsageStatQuery.ID);
+
+ mSelectionArgs3[0] = lastTimeUsed;
+ mSelectionArgs3[1] = timesUsed;
+ mSelectionArgs3[2] = String.valueOf(id);
+ db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
+ " SET " + DataUsageStatColumns.LAST_TIME_USED + "=?" +
+ "," + DataUsageStatColumns.TIMES_USED + "=?" +
+ " WHERE " + DataUsageStatColumns._ID + "=?",
+ mSelectionArgs3);
+ } else {
+ mSelectionArgs4[0] = dataId;
+ mSelectionArgs4[1] = type;
+ mSelectionArgs4[2] = timesUsed;
+ mSelectionArgs4[3] = lastTimeUsed;
+ db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
+ "(" + DataUsageStatColumns.DATA_ID +
+ "," + DataUsageStatColumns.USAGE_TYPE_INT +
+ "," + DataUsageStatColumns.TIMES_USED +
+ "," + DataUsageStatColumns.LAST_TIME_USED +
+ ") VALUES (?,?,?,?)",
+ mSelectionArgs4);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
* Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
* associated with a primary account. The primary account should be supplied from applications
* with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
@@ -9252,6 +9623,22 @@
throw new IllegalArgumentException("Invalid usage type " + type);
}
+ private static final int getAggregationType(String type, Integer defaultType) {
+ if ("TOGETHER".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_KEEP_TOGETHER; // 1
+ }
+ if ("SEPARATE".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_KEEP_SEPARATE; // 2
+ }
+ if ("UNSET".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_AUTOMATIC; // 0
+ }
+ if (defaultType != null) {
+ return defaultType;
+ }
+ throw new IllegalArgumentException("Invalid aggregation type " + type);
+ }
+
/** Use only for debug logging */
@Override
public String toString() {
diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java
index dbe8cbc..0c338a6 100644
--- a/src/com/android/providers/contacts/DataRowHandler.java
+++ b/src/com/android/providers/contacts/DataRowHandler.java
@@ -37,6 +37,9 @@
*/
public abstract class DataRowHandler {
+ private static final String[] HASH_INPUT_COLUMNS = new String[] {
+ Data.DATA1, Data.DATA2, Data.DATA15 };
+
public interface DataDeleteQuery {
public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
@@ -107,6 +110,9 @@
*/
public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
ContentValues values) {
+ // Generate hash_id from data1 and data2 columns.
+ // For photo, use data15 column instead of data1 and data2 to generate hash_id.
+ handleHashIdForInsert(values);
final long dataId = db.insert(Tables.DATA, null, values);
final Integer primary = values.getAsInteger(Data.IS_PRIMARY);
@@ -114,6 +120,7 @@
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
@@ -146,11 +153,14 @@
* @return true if update changed something
*/
public boolean update(SQLiteDatabase db, TransactionContext txContext,
- ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
+ ContentValues values, Cursor c, boolean callerIsSyncAdapter,
+ boolean callerIsMetadataSyncAdapter) {
long dataId = c.getLong(DataUpdateQuery._ID);
long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
- handlePrimaryAndSuperPrimary(values, dataId, rawContactId);
+ handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId,
+ callerIsMetadataSyncAdapter);
+ handleHashIdForUpdate(values, dataId);
if (values.size() > 0) {
mSelectionArgs1[0] = String.valueOf(dataId);
@@ -178,17 +188,70 @@
}
/**
+ * Fetch data1, data2, and data15 from values if they exist, and generate hash_id
+ * if one of data1 and data2 columns is set, otherwise using data15 instead.
+ * hash_id is null if all of these three field is null.
+ * Add hash_id key to values.
+ */
+ public void handleHashIdForInsert(ContentValues values) {
+ final String data1 = values.getAsString(Data.DATA1);
+ final String data2 = values.getAsString(Data.DATA2);
+ final byte[] data15 = values.getAsByteArray(Data.DATA15);
+ final String hashId = mDbHelper.generateHashId(data1, data2, data15);
+ if (TextUtils.isEmpty(hashId)) {
+ values.putNull(Data.HASH_ID);
+ } else {
+ values.put(Data.HASH_ID, hashId);
+ }
+ }
+
+ /**
+ * Compute hash_id column and add it to values.
+ * If both of data1 and data2 changed, using new values to compute hash_id.
+ * If one of data1 and data2 changed, read another one from DB and compute hash_id.
+ * If both data1 and data2 are null, use data15 to compute hash_id.
+ */
+ private void handleHashIdForUpdate(ContentValues values, long dataId) {
+ if (values.containsKey(Data.DATA1) || values.containsKey(Data.DATA2)
+ || values.containsKey(Data.DATA15)) {
+ String data1 = values.getAsString(Data.DATA1);
+ String data2 = values.getAsString(Data.DATA2);
+ byte[] data15 = values.getAsByteArray(Data.DATA15);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
+ HASH_INPUT_COLUMNS, Data._ID + "=?", mSelectionArgs1, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ data1 = values.containsKey(Data.DATA1) ? data1 : c.getString(0);
+ data2 = values.containsKey(Data.DATA2) ? data2 : c.getString(1);
+ data15 = values.containsKey(Data.DATA15) ? data15 : c.getBlob(2);
+ }
+ } finally {
+ c.close();
+ }
+ final String hashId = mDbHelper.generateHashId(data1, data2, data15);
+ if (TextUtils.isEmpty(hashId)) {
+ values.putNull(Data.HASH_ID);
+ } else {
+ values.put(Data.HASH_ID, hashId);
+ }
+ }
+ }
+
+ /**
* Ensures that all super-primary and primary flags of this raw_contact are
* configured correctly
*/
- private void handlePrimaryAndSuperPrimary(ContentValues values, long dataId,
- long rawContactId) {
+ private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values,
+ long dataId, long rawContactId, boolean callerIsMetadataSyncAdapter) {
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
@@ -254,6 +317,7 @@
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 063fcdb..5ae3a01 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
@@ -49,14 +49,15 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
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);
+ return super.update(db, txContext, values, c, callerIsSyncAdapter,
+ callerIsMetadataSyncAdapter);
}
/**
diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
index 539c959..3c7311f 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
@@ -50,8 +50,8 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
- if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
return false;
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
index e291986..4e9d5d4 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
@@ -85,11 +85,11 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
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)) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
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 32e9757..4d4a33f 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
@@ -46,9 +46,9 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
- super.update(db, txContext, values, c, callerIsSyncAdapter);
+ super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
// 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 03b96a3..cc85c2b 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
@@ -52,11 +52,11 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
long dataId = c.getLong(DataUpdateQuery._ID);
long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
- if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
return false;
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
index 66a3b1b..5b69fe3 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
@@ -51,8 +51,8 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
- if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
return false;
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
index 052252e..7bbac68 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
@@ -58,10 +58,10 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
fillNormalizedNumber(values);
- if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
return false;
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
index 532a852..3d28b05 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
@@ -76,7 +76,7 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
if (values.containsKey(SKIP_PROCESSING_KEY)) {
@@ -89,7 +89,7 @@
}
// Do the actual update.
- if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+ if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
return false;
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
index 044e972..b80f759 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
@@ -63,7 +63,7 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
final long dataId = c.getLong(DataUpdateQuery._ID);
final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
@@ -74,7 +74,7 @@
fixStructuredNameComponents(augmented, values);
- super.update(db, txContext, values, c, callerIsSyncAdapter);
+ super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
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 7fc97b7..235bbd7 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
@@ -60,7 +60,7 @@
@Override
public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
- Cursor c, boolean callerIsSyncAdapter) {
+ Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
final long dataId = c.getLong(DataUpdateQuery._ID);
final ContentValues augmented = getAugmentedValues(db, dataId, values);
if (augmented == null) { // No change
@@ -68,7 +68,7 @@
}
fixStructuredPostalComponents(augmented, values);
- super.update(db, txContext, values, c, callerIsSyncAdapter);
+ super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
return true;
}
diff --git a/src/com/android/providers/contacts/MetadataEntryParser.java b/src/com/android/providers/contacts/MetadataEntryParser.java
new file mode 100644
index 0000000..b90f938
--- /dev/null
+++ b/src/com/android/providers/contacts/MetadataEntryParser.java
@@ -0,0 +1,274 @@
+/*
+ * 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 {
+ final String mUsageType;
+ final long mLastTimeUsed;
+ 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 {
+ final String mDataHashId;
+ final boolean mIsPrimary;
+ final boolean mIsSuperPrimary;
+ 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 {
+ final String mBackupId;
+ final String mAccountType;
+ final String mAccountName;
+ 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 {
+ final RawContactInfo mRawContactInfo1;
+ final RawContactInfo mRawContactInfo2;
+ 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 {
+ final RawContactInfo mRawContactInfo;
+ final int mSendToVoicemail;
+ final int mStarred;
+ final int mPinned;
+ final ArrayList<FieldData> mFieldDatas;
+ 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.getBoolean(SEND_TO_VOICEMAIL);
+ final boolean starred = contactPrefs.getBoolean(STARRED);
+ final int pinned = contactPrefs.getInt(PINNED);
+
+ // 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 dfb8748..fe274a0 100644
--- a/src/com/android/providers/contacts/ProfileProvider.java
+++ b/src/com/android/providers/contacts/ProfileProvider.java
@@ -103,8 +103,8 @@
mDelegate.notifyChange();
}
- protected void notifyChange(boolean syncToNetwork) {
- mDelegate.notifyChange(syncToNetwork);
+ protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetWork) {
+ mDelegate.notifyChange(syncToNetwork, syncToMetadataNetWork);
}
protected Locale getLocale() {
diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java
index d8c93ef..fb23dfc 100644
--- a/src/com/android/providers/contacts/TransactionContext.java
+++ b/src/com/android/providers/contacts/TransactionContext.java
@@ -34,6 +34,7 @@
/** Map from raw contact id to account Id */
private HashMap<Long, Long> mInsertedRawContactsAccounts;
private HashSet<Long> mUpdatedRawContacts;
+ private HashSet<Long> mMetadataDirtyRawContacts;
private HashSet<Long> mDirtyRawContacts;
// Set used to track what has been changed and deleted. This is needed so we can update the
// contact last touch timestamp. Dirty set above is only set when sync adapter is false.
@@ -75,6 +76,15 @@
markRawContactChangedOrDeletedOrInserted(rawContactId);
}
+ public void markRawContactMetadataDirty(long rawContactId, boolean isMetadataSyncAdapter) {
+ if (!isMetadataSyncAdapter) {
+ if (mMetadataDirtyRawContacts == null) {
+ mMetadataDirtyRawContacts = Sets.newHashSet();
+ }
+ mMetadataDirtyRawContacts.add(rawContactId);
+ }
+ }
+
public void markRawContactChangedOrDeletedOrInserted(long rawContactId) {
if (mChangedRawContacts == null) {
mChangedRawContacts = Sets.newHashSet();
@@ -112,6 +122,11 @@
return mDirtyRawContacts;
}
+ public Set<Long> getMetadataDirtyRawContactIds() {
+ if (mMetadataDirtyRawContacts == null) mMetadataDirtyRawContacts = Sets.newHashSet();
+ return mMetadataDirtyRawContacts;
+ }
+
public Set<Long> getChangedRawContactIds() {
if (mChangedRawContacts == null) mChangedRawContacts = Sets.newHashSet();
return mChangedRawContacts;
@@ -147,6 +162,7 @@
mUpdatedRawContacts = null;
mUpdatedSyncStates = null;
mDirtyRawContacts = null;
+ mMetadataDirtyRawContacts = null;
mChangedRawContacts = null;
}
diff --git a/tests/assets/test1/testFileDeviceContactMetadataJSON.txt b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
new file mode 100644
index 0000000..65e624d
--- /dev/null
+++ b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
@@ -0,0 +1,69 @@
+{
+ "unique_contact_id": {
+ "account_type": "CUSTOM_ACCOUNT",
+ "custom_account_type": "facebook",
+ "account_name": "android-test",
+ "contact_id": "1111111",
+ "data_set": "FOCUS"
+ },
+ "contact_prefs": {
+ "send_to_voicemail": true,
+ "starred": false,
+ "pinned": 2
+ },
+ "aggregation_data": [
+ {
+ "type": "TOGETHER",
+ "contact_ids": [
+ {
+ "account_type": "GOOGLE_ACCOUNT",
+ "account_name": "android-test2",
+ "contact_id": "2222222",
+ "data_set": "GOOGLE_PLUS"
+ },
+ {
+ "account_type": "GOOGLE_ACCOUNT",
+ "account_name": "android-test3",
+ "contact_id": "3333333",
+ "data_set": "CUSTOM",
+ "custom_data_set": "custom type"
+ }
+ ]
+ }
+ ],
+ "field_data": [
+ {
+ "field_data_id": "1001",
+ "field_data_prefs": {
+ "is_primary": true,
+ "is_super_primary": true
+ },
+ "usage_stats": [
+ {
+ "usage_type": "CALL",
+ "last_time_used": 10000001,
+ "usage_count": 10
+ },
+ {
+ "usage_type": "SHORT_TEXT",
+ "last_time_used": 20000002,
+ "usage_count": 20
+ }
+ ]
+ },
+ {
+ "field_data_id": "1002",
+ "field_data_prefs": {
+ "is_primary": false,
+ "is_super_primary": false
+ },
+ "usage_stats": [
+ {
+ "usage_type": "LONG_TEXT",
+ "last_time_used": 30000003,
+ "usage_count": 30
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 6e389b7..9233361 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -55,7 +55,7 @@
import android.test.MoreAsserts;
import android.test.mock.MockContentResolver;
import android.util.Log;
-
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.testutil.CommonDatabaseUtils;
import com.android.providers.contacts.testutil.DataUtil;
@@ -133,6 +133,7 @@
mActor.addPermissions(
"android.permission.READ_CONTACTS",
"android.permission.WRITE_CONTACTS",
+ "android.permission.READ_WRITE_CONTACT_METADATA",
"android.permission.READ_SOCIAL_STREAM",
"android.permission.WRITE_SOCIAL_STREAM");
}
@@ -557,6 +558,18 @@
" WHERE " + BaseColumns._ID + "=" + contactId);
}
+ protected long createAccount(String accountName, String accountType, String dataSet) {
+ // There's no api for this, so we just tweak the DB directly.
+ SQLiteDatabase db = ((ContactsProvider2) getProvider()).getDatabaseHelper()
+ .getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(AccountsColumns.ACCOUNT_NAME, accountName);
+ values.put(AccountsColumns.ACCOUNT_TYPE, accountType);
+ values.put(AccountsColumns.DATA_SET, dataSet);
+ return db.insert(Tables.ACCOUNTS, null, values);
+ }
+
protected Cursor queryRawContact(long rawContactId) {
return mResolver.query(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
null, null, null, null);
@@ -817,6 +830,14 @@
c.close();
}
+ protected void assertMetadataDirty(Uri uri, boolean state) {
+ Cursor c = mResolver.query(uri, new String[]{"metadata_dirty"}, null, null, null);
+ assertTrue(c.moveToNext());
+ assertEquals(state, c.getLong(0) != 0);
+ assertFalse(c.moveToNext());
+ c.close();
+ }
+
protected long getVersion(Uri uri) {
Cursor c = mResolver.query(uri, new String[]{"version"}, null, null, null);
assertTrue(c.moveToNext());
@@ -832,6 +853,12 @@
mResolver.update(uri, values, null, null);
}
+ protected void clearMetadataDirty(Uri uri) {
+ ContentValues values = new ContentValues();
+ values.put("metadata_dirty", 0);
+ mResolver.update(uri, values, null, null);
+ }
+
protected void storeValue(Uri contentUri, long id, String column, String value) {
storeValue(ContentUris.withAppendedId(contentUri, id), column, value);
}
@@ -1290,6 +1317,10 @@
assertEquals(expected, (getContactsProvider()).isNetworkNotified());
}
+ protected void assertMetadataNetworkNotified(boolean expected) {
+ assertEquals(expected, (getContactsProvider()).isMetadataNetworkNotified());
+ }
+
protected void assertProjection(Uri uri, String[] expectedProjection) {
Cursor cursor = mResolver.query(uri, null, "0", null, null);
String[] actualProjection = cursor.getColumnNames();
diff --git a/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
new file mode 100644
index 0000000..e48e2a6
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
@@ -0,0 +1,550 @@
+/*
+ * 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.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.MetadataSync;
+import android.provider.ContactsContract.RawContacts;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.MediumTest;
+import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
+import com.android.providers.contacts.testutil.RawContactUtil;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link com.android.providers.contacts.ContactMetadataProvider}.
+ * <p/>
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.ContactMetadataProviderTest -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
+ private static String TEST_ACCOUNT_TYPE1 = "test_account_type1";
+ private static String TEST_ACCOUNT_NAME1 = "test_account_name1";
+ private static String TEST_DATA_SET1 = "plus";
+ private static String TEST_BACKUP_ID1 = "1001";
+ private static String TEST_DATA1 = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 2\n" +
+ " }\n" +
+ " }";
+
+ private static String TEST_ACCOUNT_TYPE2 = "test_account_type2";
+ private static String TEST_ACCOUNT_NAME2 = "test_account_name2";
+ private static String TEST_DATA_SET2 = null;
+ private static String TEST_BACKUP_ID2 = "1002";
+ private static String TEST_DATA2 = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE2 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME2 + ",\n" +
+ " \"contact_id\": " + TEST_BACKUP_ID2 + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 2\n" +
+ " }\n" +
+ " }";
+
+ private static String SELECTION_BY_TEST_ACCOUNT1 = MetadataSync.ACCOUNT_NAME + "='" +
+ TEST_ACCOUNT_NAME1 + "' AND " + MetadataSync.ACCOUNT_TYPE + "='" + TEST_ACCOUNT_TYPE1 +
+ "' AND " + MetadataSync.DATA_SET + "='" + TEST_DATA_SET1 + "'";
+
+ private static String SELECTION_BY_TEST_ACCOUNT2 = MetadataSync.ACCOUNT_NAME + "='" +
+ TEST_ACCOUNT_NAME2 + "' AND " + MetadataSync.ACCOUNT_TYPE + "='" + TEST_ACCOUNT_TYPE2 +
+ "' AND " + MetadataSync.DATA_SET + "='" + TEST_DATA_SET2 + "'";
+
+ private ContactMetadataProvider mContactMetadataProvider;
+ private AccountWithDataSet mTestAccount;
+ private ContentValues defaultValues;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mContactMetadataProvider = (ContactMetadataProvider) addProvider(
+ ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+ // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+ // are using different dbHelpers.
+ mContactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+ mActor.provider).getDatabaseHelper(getContext()));
+ setupData();
+ }
+
+ public void testInsertWithInvalidUri() {
+ try {
+ mResolver.insert(Uri.withAppendedPath(MetadataSync.METADATA_AUTHORITY_URI,
+ "metadata"), getDefaultValues());
+ fail("the insert was expected to fail, but it succeeded");
+ } catch (IllegalArgumentException e) {
+ // this was expected
+ }
+ }
+
+ public void testUpdateWithInvalidUri() {
+ try {
+ mResolver.update(Uri.withAppendedPath(MetadataSync.METADATA_AUTHORITY_URI,
+ "metadata"), getDefaultValues(), null, null);
+ fail("the update was expected to fail, but it succeeded");
+ } catch (IllegalArgumentException e) {
+ // this was expected
+ }
+ }
+
+ public void testGetMetadataByAccount() {
+ Cursor c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+ null, null);
+ assertEquals(1, c.getCount());
+
+ ContentValues expectedValues = defaultValues;
+ expectedValues.remove(MetadataSyncColumns.ACCOUNT_ID);
+ c.moveToFirst();
+ assertCursorValues(c, expectedValues);
+ c.close();
+ }
+
+ public void testFailOnInsertMetadataForSameAccountIdAndBackupId() {
+ // Insert a new metadata with same account and backupId as defaultValues should fail.
+ String newData = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": false,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+
+ ContentValues newValues = new ContentValues();
+ newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+ newValues.put(MetadataSync.DATA, newData);
+ newValues.put(MetadataSync.DELETED, 0);
+ try {
+ mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ public void testInsertAndUpdateMetadataSync() {
+ // Create a raw contact with backupId.
+ String backupId = "backupId10001";
+ long rawContactId = RawContactUtil.createRawContactWithAccountDataSet(
+ mResolver, mTestAccount);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.BACKUP_ID, backupId);
+ assertEquals(1, mResolver.update(rawContactUri, values, null, null));
+
+ assertStoredValue(rawContactUri, RawContacts._ID, rawContactId);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+ assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+
+ String deleted = "0";
+ String insertJson = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 2\n" +
+ " }\n" +
+ " }";
+
+ // Insert to MetadataSync table.
+ ContentValues insertedValues = new ContentValues();
+ insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ insertedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ insertedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ insertedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ insertedValues.put(MetadataSync.DATA, insertJson);
+ insertedValues.put(MetadataSync.DELETED, deleted);
+ Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+ long metadataId = ContentUris.parseId(metadataUri);
+ assertEquals(true, metadataId > 0);
+
+ // Check if RawContact table is updated after inserting metadata.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+ assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "2");
+
+ // Update the MetadataSync table.
+ String updatedJson = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": false,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+ ContentValues updatedValues = new ContentValues();
+ updatedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ updatedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ updatedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ updatedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ updatedValues.put(MetadataSync.DATA, updatedJson);
+ updatedValues.put(MetadataSync.DELETED, deleted);
+ assertEquals(1, mResolver.update(MetadataSync.CONTENT_URI, updatedValues, null, null));
+
+ // Check if the update is correct.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "0");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "0");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+ }
+
+ public void testInsertMetadataWithoutUpdateTables() {
+ // If raw contact doesn't exist, don't update raw contact tables.
+ String backupId = "newBackupId";
+ String deleted = "0";
+ String insertJson = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 2\n" +
+ " }\n" +
+ " }";
+
+ // Insert to MetadataSync table.
+ ContentValues insertedValues = new ContentValues();
+ insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ insertedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ insertedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ insertedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ insertedValues.put(MetadataSync.DATA, insertJson);
+ insertedValues.put(MetadataSync.DELETED, deleted);
+ Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+ long metadataId = ContentUris.parseId(metadataUri);
+ assertEquals(true, metadataId > 0);
+ }
+
+ public void testFailUpdateDeletedMetadata() {
+ String backupId = "backupId001";
+ String newData = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": false,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+
+ ContentValues newValues = new ContentValues();
+ newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ newValues.put(MetadataSync.DATA, newData);
+ newValues.put(MetadataSync.DELETED, 1);
+
+ try {
+ mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+ fail("the update was expected to fail, but it succeeded");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ public void testInsertWithNullData() {
+ ContentValues newValues = new ContentValues();
+ String data = null;
+ String backupId = "backupId002";
+ newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ newValues.put(MetadataSync.DATA, data);
+ newValues.put(MetadataSync.DELETED, 0);
+
+ try {
+ mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testUpdateWithNullData() {
+ ContentValues newValues = new ContentValues();
+ String data = null;
+ newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+ newValues.put(MetadataSync.DATA, data);
+ newValues.put(MetadataSync.DELETED, 0);
+
+ try {
+ mResolver.update(MetadataSync.CONTENT_URI, newValues, null, null);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testUpdateForMetadataSyncId() {
+ Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+ SELECTION_BY_TEST_ACCOUNT1, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToNext();
+ long metadataSyncId = c.getLong(0);
+ c.close();
+
+ Uri metadataUri = ContentUris.withAppendedId(MetadataSync.CONTENT_URI, metadataSyncId);
+ ContentValues newValues = new ContentValues();
+ String newData = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": false,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+ newValues.put(MetadataSync.DATA, newData);
+ newValues.put(MetadataSync.DELETED, 0);
+ assertEquals(1, mResolver.update(metadataUri, newValues, null, null));
+ assertStoredValue(metadataUri, MetadataSync.DATA, newData);
+ }
+
+ public void testDeleteMetadata() {
+ //insert another metadata for TEST_ACCOUNT
+ insertMetadata(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "2", TEST_DATA1, 0);
+ Cursor c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+ null, null);
+ assertEquals(2, c.getCount());
+ int numOfDeletion = mResolver.delete(MetadataSync.CONTENT_URI, SELECTION_BY_TEST_ACCOUNT1,
+ null);
+ assertEquals(2, numOfDeletion);
+ c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+ null, null);
+ assertEquals(0, c.getCount());
+ }
+
+ public void testBulkInsert() {
+ Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+ SELECTION_BY_TEST_ACCOUNT1, null, null);
+ assertEquals(1, c.getCount());
+
+ ContentValues values1 = getMetadataContentValues(
+ TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "123", TEST_DATA1, 0);
+ ContentValues values2 = getMetadataContentValues(
+ TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "456", TEST_DATA1, 0);
+ ContentValues[] values = new ContentValues[] {values1, values2};
+
+ mResolver.bulkInsert(MetadataSync.CONTENT_URI, values);
+ c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+ SELECTION_BY_TEST_ACCOUNT1, null, null);
+ assertEquals(3, c.getCount());
+ }
+
+ public void testBatchOperations() throws Exception {
+ // Two mentadata_sync entries in the beginning, one for TEST_ACCOUNT1 and another for
+ // TEST_ACCOUNT2
+ Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+ null, null, null);
+ assertEquals(2, c.getCount());
+
+ String updatedData = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 5\n" +
+ " }\n" +
+ " }";
+
+ String newBackupId = "2222";
+ String newData = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+ " \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+ " \"contact_id\": " + newBackupId + ",\n" +
+ " \"data_set\": \"GOOGLE_PLUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 5\n" +
+ " }\n" +
+ " }";
+
+ ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
+ ops.add(ContentProviderOperation.newUpdate(MetadataSync.CONTENT_URI)
+ .withSelection(SELECTION_BY_TEST_ACCOUNT1, null)
+ .withValue(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1)
+ .withValue(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1)
+ .withValue(MetadataSync.DATA_SET, TEST_DATA_SET1)
+ .withValue(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1)
+ .withValue(MetadataSync.DATA, updatedData)
+ .withValue(MetadataSync.DELETED, 0)
+ .build());
+
+ ops.add(ContentProviderOperation.newInsert(MetadataSync.CONTENT_URI)
+ .withValue(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1)
+ .withValue(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1)
+ .withValue(MetadataSync.DATA_SET, TEST_DATA_SET1)
+ .withValue(MetadataSync.RAW_CONTACT_BACKUP_ID, newBackupId)
+ .withValue(MetadataSync.DATA, newData)
+ .withValue(MetadataSync.DELETED, 0)
+ .build());
+
+ ops.add(ContentProviderOperation.newDelete(MetadataSync.CONTENT_URI)
+ .withSelection(SELECTION_BY_TEST_ACCOUNT2, null)
+ .build());
+
+ // Batch three operations: update the metadata_entry of TEST_ACCOUNT1; insert one new
+ // metadata_entry for TEST_ACCOUNT1; delete metadata_entry of TEST_ACCOUNT2
+ mResolver.applyBatch(MetadataSync.METADATA_AUTHORITY, ops);
+
+ // After the batch operations, there should be two metadata_entry for TEST_ACCOUNT1 with
+ // new data value and no metadata_entry for TEST_ACCOUNT2.
+ c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync.DATA},
+ SELECTION_BY_TEST_ACCOUNT1, null, null);
+ assertEquals(2, c.getCount());
+ Set<String> actualData = new HashSet<>();
+ while (c.moveToNext()) {
+ actualData.add(c.getString(0));
+ }
+ c.close();
+ MoreAsserts.assertContentsInAnyOrder(actualData, updatedData, newData);
+
+ c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+ SELECTION_BY_TEST_ACCOUNT2, null, null);
+ assertEquals(0, c.getCount());
+ }
+
+ private void setupData() {
+ mTestAccount = new AccountWithDataSet(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1,
+ TEST_DATA_SET1);
+ long rawContactId1 = RawContactUtil.createRawContactWithAccountDataSet(
+ mResolver, mTestAccount);
+ createAccount(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1);
+ insertMetadata(getDefaultValues());
+
+ // Insert another entry for another account
+ createAccount(TEST_ACCOUNT_NAME2, TEST_ACCOUNT_TYPE2, TEST_DATA_SET2);
+ insertMetadata(TEST_ACCOUNT_NAME2, TEST_ACCOUNT_TYPE2, TEST_DATA_SET2, TEST_BACKUP_ID2,
+ TEST_DATA2, 0);
+ }
+
+ private ContentValues getDefaultValues() {
+ defaultValues = new ContentValues();
+ defaultValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+ defaultValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+ defaultValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+ defaultValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+ defaultValues.put(MetadataSync.DATA, TEST_DATA1);
+ defaultValues.put(MetadataSync.DELETED, 0);
+ return defaultValues;
+ }
+
+ private long insertMetadata(String accountName, String accountType, String dataSet,
+ String backupId, String data, int deleted) {
+ return insertMetadata(getMetadataContentValues(
+ accountName, accountType, dataSet, backupId, data, deleted));
+ }
+
+ private ContentValues getMetadataContentValues(String accountName, String accountType,
+ String dataSet, String backupId, String data, int deleted) {
+ ContentValues values = new ContentValues();
+ values.put(MetadataSync.ACCOUNT_NAME, accountName);
+ values.put(MetadataSync.ACCOUNT_TYPE, accountType);
+ values.put(MetadataSync.DATA_SET, dataSet);
+ values.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ values.put(MetadataSync.DATA, data);
+ values.put(MetadataSync.DELETED, deleted);
+ return values;
+ }
+
+ private long insertMetadata(ContentValues values) {
+ return ContentUris.parseId(mResolver.insert(MetadataSync.CONTENT_URI, values));
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 7072763..c9320a5 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -56,6 +56,7 @@
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.PinnedPositions;
@@ -84,6 +85,11 @@
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+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.testutil.CommonDatabaseUtils;
import com.android.providers.contacts.testutil.ContactUtil;
import com.android.providers.contacts.testutil.DataUtil;
@@ -312,6 +318,7 @@
RawContacts.VERSION,
RawContacts.RAW_CONTACT_IS_USER_PROFILE,
RawContacts.DIRTY,
+ RawContacts.METADATA_DIRTY,
RawContacts.DELETED,
RawContacts.DISPLAY_NAME_PRIMARY,
RawContacts.DISPLAY_NAME_ALTERNATIVE,
@@ -430,6 +437,7 @@
assertProjection(Phone.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
new String[]{
Data._ID,
+ Data.HASH_ID,
Data.DATA_VERSION,
Data.IS_PRIMARY,
Data.IS_SUPER_PRIMARY,
@@ -1017,6 +1025,7 @@
values.put(RawContacts.CONTACT_ID, contactId);
assertStoredValues(dataUri, values);
+ values.remove(Photo.PHOTO);// Remove byte[] value.
assertSelection(Data.CONTENT_URI, values, Data._ID, dataId);
// Access the same data through the directory under RawContacts
@@ -1032,6 +1041,73 @@
assertNetworkNotified(true);
}
+ public void testDataInsertAndUpdate_WithHashId() {
+ long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+
+ // Insert a data.
+ ContentValues values = new ContentValues();
+ putDataValues(values, rawContactId);
+ Uri dataUri = mResolver.insert(Data.CONTENT_URI, values);
+
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ final ContactsDatabaseHelper helper = cp.getDatabaseHelper(mContext);
+ String data1 = values.getAsString(Data.DATA1);
+ String data2 = values.getAsString(Data.DATA2);
+ String combineString = data1+data2;
+ String hashId = helper.generateHashIdForData(combineString.getBytes());
+ assertStoredValue(dataUri, Data.HASH_ID, hashId);
+
+ // Update the data with primary, and check if hash_id is not changed.
+ values.remove(Data.DATA1);
+ values.remove(Data.DATA2);
+ values.remove(Data.DATA15);
+ values.put(Data.IS_PRIMARY, "1");
+ mResolver.update(dataUri, values, null, null);
+ assertStoredValue(dataUri, Data.IS_PRIMARY, "1");
+ assertStoredValue(dataUri, Data.HASH_ID, hashId);
+
+ // Update the data with new data1.
+ values = new ContentValues();
+ putDataValues(values, rawContactId);
+ String newData1 = "Newone";
+ values.put(Data.DATA1, newData1);
+ mResolver.update(dataUri, values, null, null);
+ combineString = newData1+data2;
+ String newHashId = helper.generateHashIdForData(combineString.getBytes());
+ assertStoredValue(dataUri, Data.HASH_ID, newHashId);
+
+ // Update the data with a new Data2.
+ values.remove(Data.DATA1);
+ values.put(Data.DATA2, "Newtwo");
+ combineString = "NewoneNewtwo";
+ String testHashId = helper.generateHashIdForData(combineString.getBytes());
+ mResolver.update(dataUri, values, null, null);
+ assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+ // Update the data with a new data1 + data2.
+ values.put(Data.DATA1, "one");
+ combineString = "oneNewtwo";
+ testHashId = helper.generateHashIdForData(combineString.getBytes());
+ mResolver.update(dataUri, values, null, null);
+ assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+ // Update the data with null data1 and null data2.
+ values.putNull(Data.DATA1);
+ values.putNull(Data.DATA2);
+ byte[] data15 = values.getAsByteArray(Data.DATA15);
+ testHashId = helper.generateHashIdForData(data15);
+ mResolver.update(dataUri, values, null, null);
+ assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+ // Insert a data with null data1, null data2 and null data15, should insert null hash_id.
+ putDataValues(values, rawContactId);
+ values.remove(Data.DATA1);
+ values.remove(Data.DATA2);
+ values.remove(Data.DATA15);
+ Uri dataUri2 = mResolver.insert(Data.CONTENT_URI, values);
+ assertStoredValue(dataUri2, Data.HASH_ID, null);
+ }
+
public void testDataInsertPhoneNumberTooLongIsTrimmed() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
@@ -2733,6 +2809,241 @@
assertStoredValuesOrderly(filterUri1, new ContentValues[] { v2, v1, v4, v3 });
}
+ public void testUpdateFromMetadataEntry() {
+ String accountType1 = "accountType1";
+ String accountName1 = "accountName1";
+ String dataSet1 = "plus";
+ Account account1 = new Account(accountName1, accountType1);
+ long rawContactId = RawContactUtil.createRawContactWithName(mResolver, account1);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ // Add backup_id for the raw contact.
+ String backupId = "backupId100001";
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.BACKUP_ID, backupId);
+ assertEquals(1, mResolver.update(rawContactUri, values, null, null));
+
+ String emailAddress = "address@email.com";
+ Uri dataUri = insertEmail(rawContactId, emailAddress);
+ String hashId = getStoredValue(dataUri, Data.HASH_ID);
+
+ // Another data that should not be updated.
+ String phoneNumber = "111-111-1111";
+ Uri dataUri2 = insertPhoneNumber(rawContactId, phoneNumber);
+
+ String accountType2 = "accountType2";
+ String accountName2 = "accountName2";
+ Account account2 = new Account(accountName2, accountType2);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, account2);
+ Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
+ String backupId2 = "backupId100003";
+ ContentValues values2 = new ContentValues();
+ values2.put(RawContacts.BACKUP_ID, backupId2);
+ assertEquals(1, mResolver.update(rawContactUri2, values2, null, null));
+
+ String usageTypeString = "CALL";
+ int lastTimeUsed = 1111111;
+ int timesUsed = 5;
+ String aggregationTypeString = "SEPARATE";
+ int aggregationType = AggregationExceptions.TYPE_KEEP_SEPARATE;
+
+ RawContactInfo rawContactInfo = new RawContactInfo(
+ backupId, accountType1, accountName1, null);
+ UsageStats usageStats = new UsageStats(usageTypeString, lastTimeUsed, timesUsed);
+ ArrayList<UsageStats> usageStatsList = new ArrayList<>();
+ usageStatsList.add(usageStats);
+ FieldData fieldData = new FieldData(hashId, true, true, usageStatsList);
+ ArrayList<FieldData> fieldDataList = new ArrayList<>();
+ fieldDataList.add(fieldData);
+ ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+ MetadataEntry metadataEntry = new MetadataEntry(rawContactInfo,
+ 1, 1, 1, fieldDataList, aggregationDataList);
+
+ ContactsProvider2 provider = (ContactsProvider2) getProvider();
+ final ContactsDatabaseHelper helper =
+ ((ContactsDatabaseHelper) provider.getDatabaseHelper());
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ // Before updating tables from MetadataEntry.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "0");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "0");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "0");
+ assertStoredValue(dataUri, Data.IS_PRIMARY, 0);
+ assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 0);
+
+ // Update tables without aggregation first, since aggregator will affect pinned value.
+ provider.updateFromMetaDataEntry(db, metadataEntry);
+
+ // After updating tables from MetadataEntry.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+ assertStoredValue(dataUri, Data.IS_PRIMARY, 1);
+ assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 1);
+ assertStoredValue(dataUri2, Data.IS_PRIMARY, 0);
+ assertStoredValue(dataUri2, Data.IS_SUPER_PRIMARY, 0);
+ final Uri dataUriWithUsageType = Data.CONTENT_URI.buildUpon().appendQueryParameter(
+ DataUsageFeedback.USAGE_TYPE, usageTypeString).build();
+ assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, timesUsed, lastTimeUsed);
+
+ // Update AggregationException table.
+ RawContactInfo aggregationContact = new RawContactInfo(
+ backupId2, accountType2, accountName2, null);
+ AggregationData aggregationData = new AggregationData(
+ rawContactInfo, aggregationContact, aggregationTypeString);
+ aggregationDataList.add(aggregationData);
+ metadataEntry = new MetadataEntry(rawContactInfo,
+ 1, 1, 1, fieldDataList, aggregationDataList);
+ provider.updateFromMetaDataEntry(db, metadataEntry);
+
+ // Check if AggregationException table is updated.
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID1,
+ rawContactId);
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID2,
+ rawContactId2);
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.TYPE,
+ aggregationType);
+ }
+
+ public void testUpdateMetadataOnRawContactInsert() throws Exception {
+ ContactMetadataProvider contactMetadataProvider = (ContactMetadataProvider) addProvider(
+ ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+ // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+ // are using different dbHelpers.
+ contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+ mActor.provider).getDatabaseHelper(getContext()));
+ // Create an account first.
+ String backupId = "backupId001";
+ String accountType = "accountType";
+ String accountName = "accountName";
+ Account account = new Account(accountName, accountType);
+ createAccount(accountName, accountType, null);
+
+ // Insert a metadata to MetadataSync table.
+ String data = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + accountType + ",\n" +
+ " \"account_name\": " + accountName + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"FOCUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+
+ ContentValues insertedValues = new ContentValues();
+ insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ insertedValues.put(MetadataSync.ACCOUNT_TYPE, accountType);
+ insertedValues.put(MetadataSync.ACCOUNT_NAME, accountName);
+ insertedValues.put(MetadataSync.DATA, data);
+ mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+ // Insert a raw contact.
+ long rawContactId = RawContactUtil.createRawContactWithBackupId(mResolver, backupId,
+ account);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ // Check if the raw contact is updated.
+ assertStoredValue(rawContactUri, RawContacts._ID, rawContactId);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName);
+ assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+ }
+
+ public void testDeleteMetadataOnRawContactDelete() throws Exception {
+ ContactMetadataProvider contactMetadataProvider = (ContactMetadataProvider) addProvider(
+ ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+ // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+ // are using different dbHelpers.
+ contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+ mActor.provider).getDatabaseHelper(getContext()));
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+ // Create an account first.
+ String backupId = "backupId001";
+ String accountType = "accountType";
+ String accountName = "accountName";
+ Account account = new Account(accountName, accountType);
+ createAccount(accountName, accountType, null);
+
+ // Insert a raw contact.
+ long rawContactId = RawContactUtil.createRawContactWithBackupId(mResolver, backupId,
+ account);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+ // Insert a metadata to MetadataSync table.
+ String data = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": " + accountType + ",\n" +
+ " \"account_name\": " + accountName + ",\n" +
+ " \"contact_id\": " + backupId + ",\n" +
+ " \"data_set\": \"FOCUS\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": true,\n" +
+ " \"pinned\": 1\n" +
+ " }\n" +
+ " }";
+
+ ContentValues insertedValues = new ContentValues();
+ insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+ insertedValues.put(MetadataSync.ACCOUNT_TYPE, accountType);
+ insertedValues.put(MetadataSync.ACCOUNT_NAME, accountName);
+ insertedValues.put(MetadataSync.DATA, data);
+ Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+ // Delete raw contact.
+ mResolver.delete(rawContactUri, null, null);
+ // Check if the metadata is deleted.
+ assertStoredValue(metadataUri, MetadataSync.DELETED, "1");
+ // check raw contact metadata_dirty column is not changed on raw contact deletion
+ assertMetadataDirty(rawContactUri, false);
+ // Notify metadata network on raw contact deletion
+ assertMetadataNetworkNotified(true);
+
+ // Add another rawcontact and metadata, and don't delete them.
+ // Insert a raw contact.
+ String backupId2 = "newBackupId";
+ long rawContactId2 = RawContactUtil.createRawContactWithBackupId(mResolver, backupId2,
+ account);
+ Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+ // Insert a metadata to MetadataSync table.
+ ContentValues insertedValues2 = new ContentValues();
+ insertedValues2.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId2);
+ insertedValues2.put(MetadataSync.ACCOUNT_TYPE, accountType);
+ insertedValues2.put(MetadataSync.ACCOUNT_NAME, accountName);
+ insertedValues2.put(MetadataSync.DATA, data);
+ Uri metadataUri2 = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues2);
+
+ // Update raw contact but not delete.
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.STARRED, "1");
+ mResolver.update(rawContactUri2, values, null, null);
+
+ // Check if the metadata is not marked as deleted.
+ assertStoredValue(metadataUri2, MetadataSync.DELETED, "0");
+ // check raw contact metadata_dirty column is changed on raw contact update
+ assertMetadataDirty(rawContactUri2, true);
+ // Notify metadata network on raw contact update
+ assertMetadataNetworkNotified(true);
+ }
+
public void testPostalsQuery() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Alice", "Nextore");
Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
@@ -6516,6 +6827,107 @@
assertEquals(version, getVersion(uri));
}
+ public void testMarkAsMetadataDirtyForRawContactMetadataChange() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+ Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
+ clearMetadataDirty(rawContactUri);
+
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+ mResolver.update(rawContactUri, values, null, null);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, 1);
+ assertMetadataDirty(rawContactUri, true);
+ assertMetadataNetworkNotified(true);
+ }
+
+ public void testMarkAsMetadataDirtyForAggregationExceptionChange() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, new Account("b", "b"));
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1),
+ true);
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2),
+ true);
+ assertMetadataNetworkNotified(true);
+ }
+
+ public void testMarkAsMetadataDirtyForUsageStatsChange() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "contact", "a");
+ final long did1a = ContentUris.parseId(insertEmail(rid1, "email_1_a@email.com"));
+ updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1a);
+
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rid1),
+ true);
+ assertMetadataNetworkNotified(true);
+ }
+
+ public void testMarkAsMetadataDirtyForDataPrimarySettingInsert() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+ Uri mailUri11 = insertEmail(rawContactId1, "test1@domain1.com", true, true);
+
+ assertStoredValue(mailUri11, Data.IS_PRIMARY, 1);
+ assertStoredValue(mailUri11, Data.IS_SUPER_PRIMARY, 1);
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1),
+ true);
+ assertMetadataNetworkNotified(true);
+ }
+
+ public void testMarkAsMetadataDirtyForDataPrimarySettingUpdate() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+ Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com");
+
+ assertStoredValue(mailUri1, Data.IS_PRIMARY, 0);
+ assertStoredValue(mailUri1, Data.IS_SUPER_PRIMARY, 0);
+
+ ContentValues values = new ContentValues();
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ mResolver.update(mailUri1, values, null, null);
+
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+ true);
+ assertMetadataNetworkNotified(true);
+ }
+
+ public void testMarkAsMetadataDirtyForDataDelete() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+ Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com", true, true);
+
+ mResolver.delete(mailUri1, null, null);
+
+ assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+ true);
+ assertMetadataNetworkNotified(true);
+ }
+
public void testDeleteContactWithoutName() {
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, new ContentValues());
long rawContactId = ContentUris.parseId(rawContactUri);
@@ -9103,7 +9515,7 @@
values.put(Data.DATA12, "twelve");
values.put(Data.DATA13, "thirteen");
values.put(Data.DATA14, "fourteen");
- values.put(Data.DATA15, "fifteen");
+ values.put(Data.DATA15, "fifteen".getBytes());
values.put(Data.CARRIER_PRESENCE, Data.CARRIER_PRESENCE_VT_CAPABLE);
values.put(Data.SYNC1, "sync1");
values.put(Data.SYNC2, "sync2");
diff --git a/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
new file mode 100644
index 0000000..be4df57
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+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 org.json.JSONException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Unit tests for {@link MetadataEntryParser}.
+ *
+ * Run the test like this:
+ * <code>
+ adb shell am instrument -e class com.android.providers.contacts.MetadataEntryParserTest -w \
+ com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@SmallTest
+public class MetadataEntryParserTest extends AndroidTestCase {
+
+ public void testErrorForEmptyInput() {
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry("");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testParseDataToMetadataEntry() throws IOException {
+ String contactBackupId = "1111111";
+ String accountType = "facebook";
+ String accountName = "android-test";
+ String dataSet = null;
+ int sendToVoicemail = 1;
+ int starred = 0;
+ int pinned = 2;
+ String dataHashId1 = "1001";
+ String usageType1_1 = "CALL";
+ long lastTimeUsed1_1 = 10000001;
+ int timesUsed1_1 = 10;
+ String usageType1_2 = "SHORT_TEXT";
+ long lastTimeUsed1_2 = 20000002;
+ int timesUsed1_2 = 20;
+ String dataHashId2 = "1002";
+ String usageType2 = "LONG_TEXT";
+ long lastTimeUsed2 = 30000003;
+ int timesUsed2 = 30;
+ String aggregationContactBackupId1 = "2222222";
+ String aggregationAccountType1 = "com.google";
+ String aggregationAccountName1 = "android-test2";
+ String aggregationDataSet1 = "plus";
+ String aggregationContactBackupId2 = "3333333";
+ String aggregationAccountType2 = "com.google";
+ String aggregationAccountName2 = "android-test3";
+ String aggregationDataSet2 = "custom type";
+ String type = "TOGETHER";
+ String inputFile = "test1/testFileDeviceContactMetadataJSON.txt";
+
+ RawContactInfo rawContactInfo = new RawContactInfo(
+ contactBackupId, accountType, accountName, dataSet);
+ RawContactInfo aggregationContact1 = new RawContactInfo(aggregationContactBackupId1,
+ aggregationAccountType1, aggregationAccountName1, aggregationDataSet1);
+ RawContactInfo aggregationContact2 = new RawContactInfo(aggregationContactBackupId2,
+ aggregationAccountType2, aggregationAccountName2, aggregationDataSet2);
+ AggregationData aggregationData = new AggregationData(
+ aggregationContact1, aggregationContact2, type);
+ ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+ aggregationDataList.add(aggregationData);
+
+ UsageStats usageStats1_1 = new UsageStats(usageType1_1, lastTimeUsed1_1, timesUsed1_1);
+ UsageStats usageStats1_2 = new UsageStats(usageType1_2, lastTimeUsed1_2, timesUsed1_2);
+ UsageStats usageStats2 = new UsageStats(usageType2, lastTimeUsed2, timesUsed2);
+
+ ArrayList<UsageStats> usageStats1List = new ArrayList<>();
+ usageStats1List.add(usageStats1_1);
+ usageStats1List.add(usageStats1_2);
+ FieldData fieldData1 = new FieldData(dataHashId1, true, true, usageStats1List);
+
+ ArrayList<UsageStats> usageStats2List = new ArrayList<>();
+ usageStats2List.add(usageStats2);
+ FieldData fieldData2 = new FieldData(dataHashId2, false, false, usageStats2List);
+
+ ArrayList<FieldData> fieldDataList = new ArrayList<>();
+ fieldDataList.add(fieldData1);
+ fieldDataList.add(fieldData2);
+
+ MetadataEntry expectedResult = new MetadataEntry(rawContactInfo,
+ sendToVoicemail, starred, pinned, fieldDataList, aggregationDataList);
+
+ String inputJson = readAssetAsString(inputFile);
+ MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(
+ inputJson.toString());
+ assertMetaDataEntry(expectedResult, metadataEntry);
+ }
+
+ public void testErrorForMissingContactId() {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullContactId() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAccountType() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAccountName() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullFieldDataId() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 2\n" +
+ " }," +
+ " \"field_data\": [{\n" +
+ " \"field_data_id\": \"\"}]" +
+ "}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAggregationType() throws JSONException {
+ String input = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 2\n" +
+ " },\n" +
+ " \"aggregation_data\": [\n" +
+ " {\n" +
+ " \"type\": \"\",\n" +
+ " \"contact_ids\": [\n" +
+ " {\n" +
+ " \"contact_id\": \"2222222\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"contact_id\": \"3333333\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ private String readAssetAsString(String fileName) throws IOException {
+ Context context = getTestContext();
+ InputStream input = context.getAssets().open(fileName);
+ ByteArrayOutputStream contents = new ByteArrayOutputStream();
+ int len;
+ byte[] data = new byte[1024];
+ do {
+ len = input.read(data);
+ if (len > 0) contents.write(data, 0, len);
+ } while (len == data.length);
+ return contents.toString();
+ }
+
+ private void assertMetaDataEntry(MetadataEntry entry1, MetadataEntry entry2) {
+ assertRawContactInfoEquals(entry1.mRawContactInfo, entry2.mRawContactInfo);
+ assertEquals(entry1.mSendToVoicemail, entry2.mSendToVoicemail);
+ assertEquals(entry1.mStarred, entry2.mStarred);
+ assertEquals(entry1.mPinned, entry2.mPinned);
+ assertAggregationDataListEquals(entry1.mAggregationDatas, entry2.mAggregationDatas);
+ assertFieldDataListEquals(entry1.mFieldDatas, entry2.mFieldDatas);
+ }
+
+ private void assertRawContactInfoEquals(RawContactInfo contact1, RawContactInfo contact2) {
+ assertEquals(contact1.mBackupId, contact2.mBackupId);
+ assertEquals(contact1.mAccountType, contact2.mAccountType);
+ assertEquals(contact1.mAccountName, contact2.mAccountName);
+ assertEquals(contact1.mDataSet, contact2.mDataSet);
+ }
+
+ private void assertAggregationDataListEquals(ArrayList<AggregationData> aggregationList1,
+ ArrayList<AggregationData> aggregationList2) {
+ assertEquals(aggregationList1.size(), aggregationList2.size());
+ for (int i = 0; i < aggregationList1.size(); i++) {
+ assertAggregationDataEquals(aggregationList1.get(i), aggregationList2.get(i));
+ }
+ }
+
+ private void assertAggregationDataEquals(AggregationData aggregationData1,
+ AggregationData aggregationData2) {
+ assertRawContactInfoEquals(aggregationData1.mRawContactInfo1,
+ aggregationData2.mRawContactInfo1);
+ assertRawContactInfoEquals(aggregationData1.mRawContactInfo2,
+ aggregationData2.mRawContactInfo2);
+ assertEquals(aggregationData1.mType, aggregationData2.mType);
+ }
+
+ private void assertFieldDataListEquals(ArrayList<FieldData> fieldDataList1,
+ ArrayList<FieldData> fieldDataList2) {
+ assertEquals(fieldDataList1.size(), fieldDataList2.size());
+ for (int i = 0; i < fieldDataList1.size(); i++) {
+ assertFieldDataEquals(fieldDataList1.get(i), fieldDataList2.get(i));
+ }
+ }
+
+ private void assertFieldDataEquals(FieldData fieldData1, FieldData fieldData2) {
+ assertEquals(fieldData1.mDataHashId, fieldData2.mDataHashId);
+ assertEquals(fieldData1.mIsPrimary, fieldData2.mIsPrimary);
+ assertEquals(fieldData1.mIsSuperPrimary, fieldData2.mIsSuperPrimary);
+ assertUsageStatsListEquals(fieldData1.mUsageStatsList, fieldData2.mUsageStatsList);
+ }
+
+ private void assertUsageStatsListEquals(ArrayList<UsageStats> usageStatsList1,
+ ArrayList<UsageStats> usageStatsList2) {
+ assertEquals(usageStatsList1.size(), usageStatsList2.size());
+ for (int i = 0; i < usageStatsList1.size(); i++) {
+ assertUsageStatsEquals(usageStatsList1.get(i), usageStatsList2.get(i));
+ }
+ }
+
+ private void assertUsageStatsEquals(UsageStats usageStats1, UsageStats usageStats2) {
+ assertEquals(usageStats1.mUsageType, usageStats2.mUsageType);
+ assertEquals(usageStats1.mLastTimeUsed, usageStats2.mLastTimeUsed);
+ assertEquals(usageStats1.mTimesUsed, usageStats2.mTimesUsed);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java b/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
index a55072c..8dd09bf 100644
--- a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
@@ -29,7 +29,7 @@
}
@Override
- protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
if (mDbHelper == null) {
mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
}
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 1d127c7..19878f8 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -37,11 +37,12 @@
private boolean mDataWipeEnabled = true;
private Account mAccount;
private boolean mNetworkNotified;
+ private boolean mMetadataNetworkNotified;
private boolean mIsPhone = true;
private boolean mIsVoiceCapable = true;
@Override
- protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
if (sDbHelper == null) {
sDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
}
@@ -61,17 +62,23 @@
public void onBegin() {
super.onBegin();
mNetworkNotified = false;
+ mMetadataNetworkNotified = false;
}
@Override
- protected void notifyChange(boolean syncToNetwork) {
+ protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) {
mNetworkNotified |= syncToNetwork;
+ mMetadataNetworkNotified |= syncToMetadataNetwork;
}
public boolean isNetworkNotified() {
return mNetworkNotified;
}
+ public boolean isMetadataNetworkNotified() {
+ return mMetadataNetworkNotified;
+ }
+
public void setIsPhone(boolean flag) {
mIsPhone = flag;
}
diff --git a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
index e9cd3b5..f24875b 100644
--- a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
@@ -24,6 +24,7 @@
import android.net.Uri;
import android.provider.ContactsContract;
import android.test.mock.MockContentResolver;
+import com.android.providers.contacts.AccountWithDataSet;
import java.util.List;
@@ -90,7 +91,18 @@
String... extras) {
ContentValues values = new ContentValues();
CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
- final Uri uri = TestUtil.maybeAddAccountQueryParameters(ContactsContract.RawContacts.CONTENT_URI, account);
+ final Uri uri = TestUtil.maybeAddAccountQueryParameters(
+ ContactsContract.RawContacts.CONTENT_URI, account);
+ Uri contactUri = resolver.insert(uri, values);
+ return ContentUris.parseId(contactUri);
+ }
+
+ public static long createRawContactWithAccountDataSet(ContentResolver resolver,
+ AccountWithDataSet accountWithDataSet, String... extras) {
+ ContentValues values = new ContentValues();
+ CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
+ final Uri uri = TestUtil.maybeAddAccountWithDataSetQueryParameters(
+ ContactsContract.RawContacts.CONTENT_URI, accountWithDataSet);
Uri contactUri = resolver.insert(uri, values);
return ContentUris.parseId(contactUri);
}
@@ -118,4 +130,19 @@
public static long createRawContact(ContentResolver resolver) {
return createRawContact(resolver, null);
}
+
+ public static long createRawContactWithBackupId(ContentResolver resolver, String backupId,
+ Account account) {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.RawContacts.BACKUP_ID, backupId);
+ final Uri uri = ContactsContract.RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME,
+ account.name)
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE,
+ account.type)
+ .build();
+ Uri contactUri = resolver.insert(uri, values);
+ return ContentUris.parseId(contactUri);
+ }
}
diff --git a/tests/src/com/android/providers/contacts/testutil/TestUtil.java b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
index 2020f6d..05ff61d 100644
--- a/tests/src/com/android/providers/contacts/testutil/TestUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
@@ -18,8 +18,9 @@
import android.accounts.Account;
import android.net.Uri;
-import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
import android.util.Log;
+import com.android.providers.contacts.AccountWithDataSet;
/**
* Common methods used for testing.
@@ -46,8 +47,20 @@
return uri;
}
return uri.buildUpon()
- .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
+ .build();
+ }
+
+ public static Uri maybeAddAccountWithDataSetQueryParameters(Uri uri,
+ AccountWithDataSet account) {
+ if (account == null) {
+ return uri;
+ }
+ return uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.getAccountName())
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.getAccountType())
+ .appendQueryParameter(RawContacts.DATA_SET, account.getDataSet())
.build();
}
}