diff options
45 files changed, 2371 insertions, 1418 deletions
@@ -1,20 +1,6 @@ package { - default_applicable_licenses: [ - "packages_providers_ContactsProvider_license", - ], -} - -// Added automatically by a large-scale-change -// See: http://go/android-license-faq -license { - name: "packages_providers_ContactsProvider_license", - visibility: [":__subpackages__"], - license_kinds: [ - "SPDX-license-identifier-Apache-2.0", - ], - license_text: [ - "NOTICE", - ], + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], } android_app { @@ -49,3 +35,8 @@ android_app { proguard_flags_files: ["proguard.flags"], }, } + +platform_compat_config { + name: "contacts-provider-platform-compat-config", + src: ":ContactsProvider", +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2b101bef..dbc835d3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -22,6 +22,9 @@ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" /> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> + <!-- Permissions required for reading and logging compat changes --> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" /> <permission android:name="android.permission.SEND_CALL_LOG_CHANGE" @@ -108,15 +111,6 @@ </intent-filter> </receiver> - <receiver android:name="PhoneAccountRegistrationReceiver" - android:exported="true" - android:permission="android.permission.BROADCAST_PHONE_ACCOUNT_REGISTRATION"> - <!-- Broadcast sent after a phone account is registered in telecom. --> - <intent-filter> - <action android:name="android.telecom.action.PHONE_ACCOUNT_REGISTERED"/> - </intent-filter> - </receiver> - <receiver android:name="LocaleChangeReceiver" android:exported="true"> <intent-filter> diff --git a/NOTICE b/NOTICE deleted file mode 100644 index c5b1efa7..00000000 --- a/NOTICE +++ /dev/null @@ -1,190 +0,0 @@ - - Copyright (c) 2005-2008, 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. - - 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. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - diff --git a/README-tests.md b/README-tests.md index 8351c6c3..cc828c97 100644 --- a/README-tests.md +++ b/README-tests.md @@ -3,5 +3,5 @@ Use the following command to run the unit tests. ``` -atest ContactsProviderTests ContactsProviderTests2 -```
\ No newline at end of file +atest ContactsProviderTests +``` diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 3ffd077f..5cee4f36 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -26,7 +26,7 @@ <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_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 ligações 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> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 3ffd077f..5cee4f36 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -26,7 +26,7 @@ <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_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 ligações 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> diff --git a/src/com/android/providers/contacts/AccountWithDataSet.java b/src/com/android/providers/contacts/AccountWithDataSet.java index e1f633ea..c20edce7 100644 --- a/src/com/android/providers/contacts/AccountWithDataSet.java +++ b/src/com/android/providers/contacts/AccountWithDataSet.java @@ -17,11 +17,15 @@ package com.android.providers.contacts; import android.accounts.Account; -import android.provider.ContactsContract; +import android.content.res.Resources; +import android.database.DatabaseUtils; import android.provider.ContactsContract.SimAccount; import android.text.TextUtils; +import com.android.internal.R; + import com.google.common.base.Objects; +import com.google.common.base.Strings; import java.util.List; @@ -29,7 +33,16 @@ import java.util.List; * Account information that includes the data set, if any. */ public class AccountWithDataSet { - public static final AccountWithDataSet LOCAL = new AccountWithDataSet(null, null, null); + public static final AccountWithDataSet LOCAL; + + static { + Resources resources = Resources.getSystem(); + String accountName = Strings.nullToEmpty( + resources.getString(R.string.config_rawContactsLocalAccountName)); + String accountType = Strings.nullToEmpty( + resources.getString(R.string.config_rawContactsLocalAccountType)); + LOCAL = new AccountWithDataSet(accountName, accountType, null); + } private final String mAccountName; private final String mAccountType; @@ -66,7 +79,8 @@ public class AccountWithDataSet { } public boolean isLocalAccount() { - return (mAccountName == null) && (mAccountType == null); + return LOCAL.equals(this) || ( + mAccountName == null && mAccountType == null && mDataSet == null); } @Override diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java index 22f1cad4..c5052d70 100644 --- a/src/com/android/providers/contacts/CallLogDatabaseHelper.java +++ b/src/com/android/providers/contacts/CallLogDatabaseHelper.java @@ -18,28 +18,38 @@ package com.android.providers.contacts; import android.annotation.Nullable; import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.preference.PreferenceManager; import android.provider.CallLog.Calls; import android.provider.VoicemailContract; import android.provider.VoicemailContract.Status; import android.provider.VoicemailContract.Voicemails; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import com.android.providers.contacts.util.PropertyUtils; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * SQLite database (helper) for {@link CallLogProvider} and {@link VoicemailContentProvider}. */ public class CallLogDatabaseHelper { private static final String TAG = "CallLogDatabaseHelper"; - private static final int DATABASE_VERSION = 10; + @VisibleForTesting + static final int DATABASE_VERSION = 11; private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE @@ -58,6 +68,9 @@ public class CallLogDatabaseHelper { private final OpenHelper mOpenHelper; + @VisibleForTesting + final PhoneAccountHandleMigrationUtils mPhoneAccountHandleMigrationUtils; + public interface Tables { String CALLS = "calls"; String VOICEMAIL_STATUS = "voicemail_status"; @@ -74,7 +87,7 @@ public class CallLogDatabaseHelper { * * DO NOT CHANCE ANY OF THE CONSTANTS. */ - private interface LegacyConstants { + public interface LegacyConstants { /** Table name used in the contacts DB.*/ String CALLS_LEGACY = "calls"; @@ -85,7 +98,8 @@ public class CallLogDatabaseHelper { String CALL_LOG_LAST_SYNCED_LEGACY = "call_log_last_synced"; } - private final class OpenHelper extends SQLiteOpenHelper { + @VisibleForTesting + public class OpenHelper extends SQLiteOpenHelper { public OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); @@ -157,7 +171,7 @@ public class CallLogDatabaseHelper { Calls.SUBJECT + " TEXT," + Calls.LOCATION + " TEXT," + Calls.COMPOSER_PHOTO_URI + " TEXT," + - + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " INTEGER NOT NULL DEFAULT 0," + Voicemails._DATA + " TEXT," + Voicemails.HAS_CONTENT + " INTEGER," + Voicemails.MIME_TYPE + " TEXT," + @@ -233,12 +247,23 @@ public class CallLogDatabaseHelper { if (oldVersion < 10) { upgradeToVersion10(db); } + + if (oldVersion < 11) { + upgradeToVersion11(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Ignore } } @VisibleForTesting CallLogDatabaseHelper(Context context, String databaseName) { mContext = context; + mPhoneAccountHandleMigrationUtils = new PhoneAccountHandleMigrationUtils( + context, PhoneAccountHandleMigrationUtils.TYPE_CALL_LOG); mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION); } @@ -275,6 +300,31 @@ public class CallLogDatabaseHelper { } /** + * Updates phone account migration pending status, indicating if there is any phone account + * handle that need to migrate. Called in CallLogProvider. + */ + void updatePhoneAccountHandleMigrationPendingStatus() { + mPhoneAccountHandleMigrationUtils.updatePhoneAccountHandleMigrationPendingStatus( + getWritableDatabase()); + } + + /** + * Migrate all the pending phone account handles based on the given iccId and subId. Used + * by CallLogProvider. + */ + void migratePendingPhoneAccountHandles(String iccId, String subId) { + mPhoneAccountHandleMigrationUtils.migratePendingPhoneAccountHandles( + iccId, subId, getWritableDatabase()); + } + + /** + * Try to migrate any PhoneAccountId to SubId from IccId. Used by CallLogProvider. + */ + void migrateIccIdToSubId() { + mPhoneAccountHandleMigrationUtils.migrateIccIdToSubId(getWritableDatabase()); + } + + /** * Add the {@link Calls.VIA_NUMBER} Column to the CallLog Database. */ private void upgradeToVersion2(SQLiteDatabase db) { @@ -473,6 +523,15 @@ public class CallLogDatabaseHelper { db.execSQL("ALTER TABLE calls ADD location TEXT"); db.execSQL("ALTER TABLE calls ADD composer_photo_uri TEXT"); } + + private void upgradeToVersion11(SQLiteDatabase db) { + // Create colums for IS_PHONE_ACCOUNT_MIGRATION_PENDING + db.execSQL("ALTER TABLE calls ADD is_call_log_phone_account_migration_pending" + + " INTEGER NOT NULL DEFAULT 0"); + mPhoneAccountHandleMigrationUtils.markAllTelephonyPhoneAccountsPendingMigration(db); + mPhoneAccountHandleMigrationUtils.migrateIccIdToSubId(db); + } + /** * Perform the migration from the contacts2.db (of the latest version) to the current calllog/ * voicemail status tables. @@ -567,6 +626,10 @@ public class CallLogDatabaseHelper { return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase(); } + public PhoneAccountHandleMigrationUtils getPhoneAccountHandleMigrationUtils() { + return mPhoneAccountHandleMigrationUtils; + } + public ArraySet<String> selectDistinctColumn(String table, String column) { final ArraySet<String> ret = new ArraySet<>(); final SQLiteDatabase db = getReadableDatabase(); @@ -599,4 +662,9 @@ public class CallLogDatabaseHelper { public void wipeForTest() { getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS); } + + @VisibleForTesting + OpenHelper getOpenHelper() { + return mOpenHelper; + } } diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java index 7d4817cb..da2d0b8a 100644 --- a/src/com/android/providers/contacts/CallLogProvider.java +++ b/src/com/android/providers/contacts/CallLogProvider.java @@ -19,10 +19,12 @@ package com.android.providers.contacts; import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause; +import static com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; +import android.content.BroadcastReceiver; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; @@ -30,6 +32,8 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; @@ -50,6 +54,8 @@ import android.provider.CallLog.Calls; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -74,13 +80,12 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.DirectoryStream; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -96,8 +101,10 @@ public class CallLogProvider extends ContentProvider { public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); - private static final int BACKGROUND_TASK_INITIALIZE = 0; + @VisibleForTesting + protected static final int BACKGROUND_TASK_INITIALIZE = 0; private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1; + private static final int BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES = 2; /** Selection clause for selecting all calls that were made after a certain time */ private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?"; @@ -245,8 +252,43 @@ public class CallLogProvider extends ContentProvider { sCallsProjectionMap.put(Calls.COMPOSER_PHOTO_URI, Calls.COMPOSER_PHOTO_URI); sCallsProjectionMap.put(Calls.SUBJECT, Calls.SUBJECT); sCallsProjectionMap.put(Calls.LOCATION, Calls.LOCATION); + sCallsProjectionMap.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING); } + /** + * Subscription change will trigger ACTION_PHONE_ACCOUNT_REGISTERED that broadcasts new + * PhoneAccountHandle that is created based on the new subscription. This receiver is used + * for listening new subscription change and migrating phone account handle if any pending. + * + * It is then used by the call log to un-hide any entries which were previously hidden after + * a backup-restore until its associated phone-account is registered with telecom. After a + * restore, we hide call log entries until the user inserts the corresponding SIM, registers + * the corresponding SIP account, or registers a corresponding alternative phone-account. + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(intent.getAction())) { + PhoneAccountHandle phoneAccountHandle = + (PhoneAccountHandle) intent.getParcelableExtra( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); + if (mDbHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending() + && TELEPHONY_COMPONENT_NAME.equals( + phoneAccountHandle.getComponentName().flattenToString()) + && !mMigratedPhoneAccountHandles.contains(phoneAccountHandle)) { + mMigratedPhoneAccountHandles.add(phoneAccountHandle); + mTaskScheduler.scheduleTask( + BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES, phoneAccountHandle); + } else { + mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, + phoneAccountHandle); + } + } + } + }; + private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts"; @VisibleForTesting @@ -262,7 +304,8 @@ public class CallLogProvider extends ContentProvider { private ContactsTaskScheduler mTaskScheduler; - private volatile CountDownLatch mReadAccessLatch; + @VisibleForTesting + protected volatile CountDownLatch mReadAccessLatch; private CallLogDatabaseHelper mDbHelper; private DatabaseUtils.InsertHelper mCallsInserter; @@ -270,10 +313,12 @@ public class CallLogProvider extends ContentProvider { private int mMinMatch; private VoicemailPermissions mVoicemailPermissions; private CallLogInsertionHelper mCallLogInsertionHelper; + private SubscriptionManager mSubscriptionManager; private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>(); private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>(); private final ProviderAccessStats mStats = new ProviderAccessStats(); + private final Set<PhoneAccountHandle> mMigratedPhoneAccountHandles = new HashSet<>(); protected boolean isShadow() { return false; @@ -316,6 +361,13 @@ public class CallLogProvider extends ContentProvider { mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + + // Register a receiver to hear sim change event for migrating pending + // PhoneAccountHandle ID or/and unhides restored call logs + IntentFilter filter = new IntentFilter(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED); + context.registerReceiver(mBroadcastReceiver, filter); + if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish"); } @@ -337,6 +389,25 @@ public class CallLogProvider extends ContentProvider { return mMinMatch; } + @NeededForTesting + public CallLogDatabaseHelper getCallLogDatabaseHelperForTest() { + return mDbHelper; + } + + @NeededForTesting + public void setCallLogDatabaseHelperForTest(CallLogDatabaseHelper callLogDatabaseHelper) { + mDbHelper = callLogDatabaseHelper; + } + + /** + * @return the currently registered BroadcastReceiver for listening + * ACTION_PHONE_ACCOUNT_REGISTERED in the current process. + */ + @NeededForTesting + public BroadcastReceiver getBroadcastReceiverForTest() { + return mBroadcastReceiver; + } + protected CallLogDatabaseHelper getDatabaseHelper(final Context context) { return CallLogDatabaseHelper.getInstance(context); } @@ -921,10 +992,6 @@ public class CallLogProvider extends ContentProvider { } } - void adjustForNewPhoneAccount(PhoneAccountHandle handle) { - mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle); - } - /** * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications * after the operation is performed. @@ -1011,15 +1078,13 @@ public class CallLogProvider extends ContentProvider { } final UserManager userManager = UserUtils.getUserManager(getContext()); + final int myUserId = userManager.getProcessUserId(); // TODO: http://b/24944959 - if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager, - userManager.getUserHandle())) { + if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager, myUserId)) { return; } - final int myUserId = userManager.getUserHandle(); - // See the comment in Calls.addCall() for the logic. if (userManager.isSystemUser()) { @@ -1134,7 +1199,6 @@ public class CallLogProvider extends ContentProvider { // Keep going and get as many as we can. } } - } /** * Un-hides any hidden call log entries that are associated with the specified handle. @@ -1174,7 +1238,6 @@ public class CallLogProvider extends ContentProvider { cursor.close(); } } - } /** @@ -1266,16 +1329,42 @@ public class CallLogProvider extends ContentProvider { } } - private void performBackgroundTask(int task, Object arg) { + @VisibleForTesting + protected void performBackgroundTask(int task, Object arg) { if (task == BACKGROUND_TASK_INITIALIZE) { try { + mDbHelper.updatePhoneAccountHandleMigrationPendingStatus(); + if (mDbHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending()) { + Log.i(TAG, "performBackgroundTask for pending PhoneAccountHandle migration"); + mDbHelper.migrateIccIdToSubId(); + } syncEntries(); } finally { mReadAccessLatch.countDown(); - mReadAccessLatch = null; } } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) { + Log.i(TAG, "performBackgroundTask for unhide PhoneAccountHandles"); adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg); + } else if (task == BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES) { + PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) arg; + String iccId = null; + try { + SubscriptionInfo info = mSubscriptionManager.getActiveSubscriptionInfo( + Integer.parseInt(phoneAccountHandle.getId())); + if (info != null) { + iccId = info.getIccId(); + } + } catch (NumberFormatException nfe) { + // Ignore the exception, iccId will remain null and be handled below. + } + if (iccId == null) { + Log.i(TAG, "ACTION_PHONE_ACCOUNT_REGISTERED received null IccId."); + } else { + Log.i(TAG, "ACTION_PHONE_ACCOUNT_REGISTERED received for migrating phone" + + " account handle SubId: " + phoneAccountHandle.getId()); + mDbHelper.migratePendingPhoneAccountHandles(iccId, phoneAccountHandle.getId()); + } } } diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java index 7f4188d2..802b2488 100644 --- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java +++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java @@ -16,6 +16,7 @@ package com.android.providers.contacts; +import android.accounts.Account; import android.app.ActivityManager; import android.content.ContentResolver; import android.content.ContentValues; @@ -42,6 +43,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.SystemClock; import android.os.UserManager; +import android.preference.PreferenceManager; import android.provider.BaseColumns; import android.provider.ContactsContract; import android.provider.ContactsContract.AggregationExceptions; @@ -91,6 +93,7 @@ import android.util.Log; import android.util.Slog; import com.android.common.content.SyncStateContentProviderHelper; +import com.android.internal.R; import com.android.internal.R.bool; import com.android.internal.annotations.VisibleForTesting; import com.android.providers.contacts.aggregation.util.CommonNicknameCache; @@ -101,14 +104,19 @@ import com.android.providers.contacts.sqlite.DatabaseAnalyzer; import com.android.providers.contacts.sqlite.SqlChecker; import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException; import com.android.providers.contacts.util.NeededForTesting; +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import com.android.providers.contacts.util.PropertyUtils; +import com.google.common.base.Strings; + import java.io.PrintWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; @@ -143,9 +151,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { * 1300-1399 P * 1400-1499 Q * 1500-1599 S + * 1600-1699 T * </pre> */ - static final int DATABASE_VERSION = 1501; + static final int DATABASE_VERSION = 1604; private static final int MINIMUM_SUPPORTED_VERSION = 700; @VisibleForTesting @@ -165,6 +174,16 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { private static final String RUSSIA_COUNTRY_CODE = "RU"; private static final String KAZAKHSTAN_COUNTRY_CODE = "KZ"; + /** + * Max size for "simple" fields, such as names, phone numbers and email addresses. + */ + private static final int SIMPLE_FIELD_MAX_SIZE_DEFAULT = 10 * 1024; + private static final String SIMPLE_FIELD_MAX_SIZE_KEY = "simple_field_max_size"; + private static volatile Integer sSimpleFieldMaxSizeCached = null; + + private static final long DEVICE_CONFIG_CACHE_EXPIRATION_MS = 1 * 60 * 60 * 1000; // 1 hour + private static volatile long sDeviceConfigCacheExpirationElapsedTime; + public interface Tables { public static final String CONTACTS = "contacts"; public static final String DELETED_CONTACTS = "deleted_contacts"; @@ -224,37 +243,21 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + ")"; // NOTE: This requires late binding of GroupMembership MIME-type - // TODO Consolidate settings and accounts public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = Tables.RAW_CONTACTS + " JOIN " + Tables.ACCOUNTS + " ON (" + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")" - + "LEFT OUTER JOIN " + Tables.SETTINGS + " ON (" - + AccountsColumns.CONCRETE_ACCOUNT_NAME + "=" - + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND " - + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "=" - + SettingsColumns.CONCRETE_ACCOUNT_TYPE + " AND " - + "((" + AccountsColumns.CONCRETE_DATA_SET + " IS NULL AND " - + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR (" - + AccountsColumns.CONCRETE_DATA_SET + "=" - + SettingsColumns.CONCRETE_DATA_SET + "))) " + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND " + "data.raw_contact_id = raw_contacts._id) " + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID + ")"; // NOTE: This requires late binding of GroupMembership MIME-type - // TODO Add missing DATA_SET join -- or just consolidate settings and accounts - public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings " + public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "accounts " + "LEFT OUTER JOIN raw_contacts ON (" - + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=(SELECT " + + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID - + " FROM " + Tables.ACCOUNTS - + " WHERE " - + "(" + AccountsColumns.CONCRETE_ACCOUNT_NAME - + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + ") AND " - + "(" + AccountsColumns.CONCRETE_ACCOUNT_TYPE - + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + ")))" + + ")" + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND " + "data.raw_contact_id = raw_contacts._id) " + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; @@ -340,6 +343,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { public static final String ENTITIES = "view_entities"; public static final String RAW_ENTITIES = "view_raw_entities"; public static final String GROUPS = "view_groups"; + public static final String SETTINGS = "view_settings"; /** The data_usage_stat table with the low-res columns. */ public static final String DATA_USAGE_LR = "view_data_usage"; @@ -368,21 +372,30 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } public interface Clauses { + final String HAVING_NO_GROUPS = "COUNT(" + DataColumns.CONCRETE_GROUP_ID + ") == 0"; - final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + "," - + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID; + final String GROUP_BY_ACCOUNT_CONTACT_ID = AccountsColumns.CONCRETE_ID + "," + + RawContacts.CONTACT_ID; String LOCAL_ACCOUNT_ID = - "(SELECT " + AccountsColumns._ID + - " FROM " + Tables.ACCOUNTS + - " WHERE " + - AccountsColumns.ACCOUNT_NAME + " IS NULL AND " + - AccountsColumns.ACCOUNT_TYPE + " IS NULL AND " + - AccountsColumns.DATA_SET + " IS NULL)"; - - final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_ID - + "=" + LOCAL_ACCOUNT_ID; + "(SELECT " + + AccountsColumns._ID + + " FROM " + + Tables.ACCOUNTS + + " WHERE " + + AccountsColumns.ACCOUNT_NAME + + " IS " + + MoreDatabaseUtils.sqlEscapeNullableString( + AccountWithDataSet.LOCAL.getAccountName()) + + " AND " + + AccountsColumns.ACCOUNT_TYPE + + " IS " + + MoreDatabaseUtils.sqlEscapeNullableString( + AccountWithDataSet.LOCAL.getAccountType()) + + " AND " + + AccountsColumns.DATA_SET + + " IS NULL)"; final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0"; @@ -393,8 +406,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { "SELECT " + "MAX((SELECT (CASE WHEN " + "(CASE" + - " WHEN " + RAW_CONTACT_IS_LOCAL + - " THEN 1 " + " WHEN " + ZERO_GROUP_MEMBERSHIPS + " THEN " + Settings.UNGROUPED_VISIBLE + " ELSE MAX(" + Groups.GROUP_VISIBLE + ")" + @@ -417,6 +428,17 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { "EXISTS (SELECT _id FROM " + Tables.DEFAULT_DIRECTORY + " WHERE " + Tables.CONTACTS +"." + Contacts._ID + "=" + Tables.DEFAULT_DIRECTORY +"." + Contacts._ID + ")"; + + // Settings are in the accounts table and should only be deletable if there are no + // raw contacts or groups remaining in the account. + public static final String DELETABLE_SETTINGS = + "NOT EXISTS (SELECT 1 FROM " + Tables.RAW_CONTACTS + + " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + + ViewSettingsColumns.CONCRETE_ACCOUNT_ID + + " UNION SELECT 1 FROM " + Tables.GROUPS + + " WHERE " + GroupsColumns.ACCOUNT_ID + "=" + + ViewSettingsColumns.CONCRETE_ACCOUNT_ID + + ")"; } public interface ContactsColumns { @@ -559,12 +581,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { public static final String ACCOUNT_ID = "account_id"; public static final String CONCRETE_ACCOUNT_ID = Tables.GROUPS + "." + ACCOUNT_ID; - } - public interface ViewGroupsColumns { - String CONCRETE_ACCOUNT_NAME = Views.GROUPS + "." + Groups.ACCOUNT_NAME; - String CONCRETE_ACCOUNT_TYPE = Views.GROUPS + "." + Groups.ACCOUNT_TYPE; - String CONCRETE_DATA_SET = Views.GROUPS + "." + Groups.DATA_SET; + public static final String CONCRETE_SHOULD_SYNC = Tables.GROUPS + "." + Groups.SHOULD_SYNC; } public interface ActivitiesColumns { @@ -611,13 +629,9 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { public static final String CLUSTER = "cluster"; } - public interface SettingsColumns { - public static final String CONCRETE_ACCOUNT_NAME = Tables.SETTINGS + "." - + Settings.ACCOUNT_NAME; - public static final String CONCRETE_ACCOUNT_TYPE = Tables.SETTINGS + "." - + Settings.ACCOUNT_TYPE; - public static final String CONCRETE_DATA_SET = Tables.SETTINGS + "." - + Settings.DATA_SET; + public interface ViewSettingsColumns { + public static final String ACCOUNT_ID = "account_id"; + public static final String CONCRETE_ACCOUNT_ID = Views.SETTINGS + "." + ACCOUNT_ID; } public interface PresenceColumns { @@ -703,10 +717,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { String DATA_SET = RawContacts.DATA_SET; String SIM_SLOT_INDEX = "sim_slot_index"; String SIM_EF_TYPE = "sim_ef_type"; + String UNGROUPED_VISIBLE = Settings.UNGROUPED_VISIBLE; + String SHOULD_SYNC = Settings.SHOULD_SYNC; + String IS_DEFAULT = Settings.IS_DEFAULT; String CONCRETE_ACCOUNT_NAME = Tables.ACCOUNTS + "." + ACCOUNT_NAME; String CONCRETE_ACCOUNT_TYPE = Tables.ACCOUNTS + "." + ACCOUNT_TYPE; String CONCRETE_DATA_SET = Tables.ACCOUNTS + "." + DATA_SET; + } public interface DirectoryColumns { @@ -933,6 +951,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { private final boolean mIsTestInstance; private final SyncStateContentProviderHelper mSyncState; private final CountryMonitor mCountryMonitor; + private final PhoneAccountHandleMigrationUtils mPhoneAccountHandleMigrationUtils; /** * Time when the DB was created. It's persisted in {@link DbProperties#DATABASE_TIME_CREATED}, @@ -983,10 +1002,16 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { return new ContactsDatabaseHelper(context, filename, false, /* isTestInstance=*/ true); } + public PhoneAccountHandleMigrationUtils getPhoneAccountHandleMigrationUtils() { + return mPhoneAccountHandleMigrationUtils; + } + protected ContactsDatabaseHelper( Context context, String databaseName, boolean optimizationEnabled, boolean isTestInstance) { super(context, databaseName, null, DATABASE_VERSION, MINIMUM_SUPPORTED_VERSION, null); + mPhoneAccountHandleMigrationUtils = new PhoneAccountHandleMigrationUtils( + context, PhoneAccountHandleMigrationUtils.TYPE_CONTACTS); boolean enableWal = android.provider.Settings.Global.getInt(context.getContentResolver(), android.provider.Settings.Global.CONTACTS_DATABASE_WAL_ENABLED, 1) == 1; if (dbForProfile() != 0 || ActivityManager.isLowRamDeviceStatic()) { @@ -999,7 +1024,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { mIsTestInstance = isTestInstance; mContext = context; mSyncState = new SyncStateContentProviderHelper(); - mCountryMonitor = new CountryMonitor(context, this::updateUseStrictPhoneNumberComparison); startListeningToDeviceConfigUpdates(); @@ -1125,7 +1149,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } } - private void createPresenceTables(SQLiteDatabase db) { + @VisibleForTesting + void createPresenceTables(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.PRESENCE + " ("+ StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," + StatusUpdates.PROTOCOL + " INTEGER NOT NULL," + @@ -1224,8 +1249,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { AccountsColumns.ACCOUNT_TYPE + " TEXT, " + AccountsColumns.DATA_SET + " TEXT, " + AccountsColumns.SIM_SLOT_INDEX + " INTEGER, " + - AccountsColumns.SIM_EF_TYPE + " INTEGER" + - ");"); + AccountsColumns.SIM_EF_TYPE + " INTEGER, " + + AccountsColumns.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," + + AccountsColumns.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," + + AccountsColumns.IS_DEFAULT + " INTEGER NOT NULL DEFAULT 0" + ");"); // Note, there are two sets of the usage stat columns: LR_* and RAW_*. // RAW_* contain the real values, which clients can't access. The column names start @@ -1423,6 +1450,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { Data.SYNC3 + " TEXT, " + Data.SYNC4 + " TEXT, " + Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0, " + + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " INTEGER NOT NULL DEFAULT 0, " + Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + " TEXT, " + Data.PREFERRED_PHONE_ACCOUNT_ID + " TEXT " + ");"); @@ -1551,14 +1579,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { AggregationExceptions.RAW_CONTACT_ID1 + ");"); - db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" + - Settings.ACCOUNT_NAME + " STRING NOT NULL," + - Settings.ACCOUNT_TYPE + " STRING NOT NULL," + - Settings.DATA_SET + " STRING," + - Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," + - Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1" + - ");"); - db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" + Contacts._ID + " INTEGER PRIMARY KEY" + ");"); @@ -1607,6 +1627,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { // When adding new tables, be sure to also add size-estimates in updateSqliteStats createContactsViews(db); createGroupsView(db); + createSettingsView(db); createContactsTriggers(db); createContactsIndexes(db, false /* we build stats table later */); createPresenceTables(db); @@ -1776,14 +1797,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + " WHERE " + Groups._ID + "=OLD." + Groups._ID + ";" + " END"); - // Update DEFAULT_FILTER table per AUTO_ADD column update, see upgradeToVersion411. - final String insertContactsWithoutAccount = ( - " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + - " SELECT " + RawContacts.CONTACT_ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + - "=" + Clauses.LOCAL_ACCOUNT_ID + ";"); - final String insertContactsWithAccountNoDefaultGroup = ( " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + " SELECT " + RawContacts.CONTACT_ID + @@ -1818,7 +1831,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + " AFTER UPDATE OF " + Groups.AUTO_ADD + " ON " + Tables.GROUPS + " BEGIN " + " DELETE FROM " + Tables.DEFAULT_DIRECTORY + ";" - + insertContactsWithoutAccount + insertContactsWithAccountNoDefaultGroup + insertContactsWithAccountDefaultGroup + " END"); @@ -2252,7 +2264,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + Groups.SYSTEM_ID + "," + Groups.DELETED + "," + Groups.GROUP_VISIBLE + "," - + Groups.SHOULD_SYNC + "," + + GroupsColumns.CONCRETE_SHOULD_SYNC + " AS " + Groups.SHOULD_SYNC + "," + Groups.AUTO_ADD + "," + Groups.FAVORITES + "," + Groups.GROUP_IS_READ_ONLY + "," @@ -2274,6 +2286,53 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { db.execSQL("CREATE VIEW " + Views.GROUPS + " AS " + groupsSelect); } + private void createSettingsView(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS " + Views.SETTINGS + "_update;"); + db.execSQL("DROP TRIGGER IF EXISTS " + Tables.ACCOUNTS + "_insert_local_account "); + db.execSQL("DROP VIEW IF EXISTS " + Views.SETTINGS + ";"); + + String settingsColumns = AccountsColumns.CONCRETE_ID + + " AS " + ViewSettingsColumns.ACCOUNT_ID + "," + + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + Settings.ACCOUNT_NAME + "," + + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + Settings.ACCOUNT_TYPE + "," + + AccountsColumns.CONCRETE_DATA_SET + " AS " + Settings.DATA_SET + "," + + Settings.UNGROUPED_VISIBLE + "," + + Settings.SHOULD_SYNC; + + String settingsSelect = "SELECT " + settingsColumns + " FROM " + Tables.ACCOUNTS; + + db.execSQL("CREATE VIEW " + Views.SETTINGS + " AS " + settingsSelect); + + // A trigger is used to update settings to prevent changing the other columns in the + // accounts table that are not settings related. + db.execSQL("CREATE TRIGGER " + Views.SETTINGS + "_update " + + "INSTEAD OF UPDATE ON " + Views.SETTINGS + " " + + "BEGIN UPDATE " + Tables.ACCOUNTS + " SET " + + AccountsColumns.UNGROUPED_VISIBLE + " = NEW." + + Settings.UNGROUPED_VISIBLE + ", " + + AccountsColumns.SHOULD_SYNC + " = NEW." + Settings.SHOULD_SYNC + " " + + "WHERE _id = OLD." + ViewSettingsColumns.ACCOUNT_ID + "; " + + "END;"); + + // Unlike other accounts ungrouped contacts in the local account are visible by default and + // it is not syncable. + String localAccountNameSqlLiteral = MoreDatabaseUtils.sqlEscapeNullableString( + AccountWithDataSet.LOCAL.getAccountName()); + String localAccountTypeSqlLiteral = MoreDatabaseUtils.sqlEscapeNullableString( + AccountWithDataSet.LOCAL.getAccountType()); + db.execSQL("CREATE TRIGGER " + Tables.ACCOUNTS + "_insert_local_account " + + "AFTER INSERT ON " + Tables.ACCOUNTS + " " + + "WHEN NEW." + AccountsColumns.ACCOUNT_NAME + " IS " + localAccountNameSqlLiteral + + " AND NEW." + AccountsColumns.ACCOUNT_TYPE + " IS " + localAccountTypeSqlLiteral + + " AND NEW." + AccountsColumns.DATA_SET + " IS NULL " + + "BEGIN UPDATE " + Tables.ACCOUNTS + " SET " + + Settings.UNGROUPED_VISIBLE + " = 1, " + + Settings.SHOULD_SYNC + " = 0 " + + "WHERE " + AccountsColumns._ID + " = NEW." + AccountsColumns._ID + "; " + + "END;" + ); + } + @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i(TAG, "ContactsProvider cannot proceed because downgrading your database is not " + @@ -2594,12 +2653,44 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { oldVersion = 1501; } + if (isUpgradeRequired(oldVersion, newVersion, 1600)) { + upgradeToVersion1600(db); + upgradeViewsAndTriggers = true; + oldVersion = 1600; + } + + if (isUpgradeRequired(oldVersion, newVersion, 1601)) { + upgradeToVersion1601(db); + upgradeViewsAndTriggers = true; + oldVersion = 1601; + } + + if (isUpgradeRequired(oldVersion, newVersion, 1602)) { + // 1602 was used for an upgrade that was reverted and is now a no-op. It is safe to skip + // it but the database version should not be reused because droidfood devices may have + // run the upgrade. + oldVersion = 1602; + } + + if (isUpgradeRequired(oldVersion, newVersion, 1603)) { + upgradeToVersion1603(db); + upgradeViewsAndTriggers = true; + oldVersion = 1603; + } + + if (isUpgradeRequired(oldVersion, newVersion, 1604)) { + upgradeToVersion1604(db); + upgradeViewsAndTriggers = true; + oldVersion = 1604; + } + // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here // yet, until CallLogDatabaseHelper moves the data. if (upgradeViewsAndTriggers) { createContactsViews(db); createGroupsView(db); + createSettingsView(db); createContactsTriggers(db); createContactsIndexes(db, false /* we build stats table later */); upgradeLegacyApiSupport = true; @@ -3123,7 +3214,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { private void upgradeToVersion910(SQLiteDatabase db) { final UserManager userManager = (UserManager) mContext.getSystemService( Context.USER_SERVICE); - final UserInfo user = userManager.getUserInfo(userManager.getUserHandle()); + final UserInfo user = userManager.getUserInfo(userManager.getProcessUserId()); if (user.isManagedProfile()) { db.execSQL("DELETE FROM calls;"); } @@ -3375,6 +3466,89 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } } + private void upgradeToVersion1600(SQLiteDatabase db) { + db.execSQL("ALTER TABLE accounts ADD ungrouped_visible INTEGER NOT NULL DEFAULT 0;"); + db.execSQL("ALTER TABLE accounts ADD should_sync INTEGER NOT NULL DEFAULT 1;"); + + ContentValues values = new ContentValues(); + // Copy over the existing settings rows. + try (Cursor cursor = db.query("settings", new String[]{ + Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE, Settings.DATA_SET, + Settings.UNGROUPED_VISIBLE, Settings.SHOULD_SYNC + }, null, null, null, null, null)) { + String[] selectionArgs = new String[3]; + while (cursor.moveToNext()) { + DatabaseUtils.cursorRowToContentValues(cursor, values); + selectionArgs[0] = values.getAsString(Settings.ACCOUNT_NAME); + selectionArgs[1] = values.getAsString(Settings.ACCOUNT_TYPE); + selectionArgs[2] = values.getAsString(Settings.DATA_SET); + if (values.getAsString(Settings.DATA_SET) != null) { + db.update("accounts", values, + "account_name = ? AND account_type = ? AND data_set = ?", + selectionArgs); + } else { + db.update("accounts", values, + "account_name = ? AND account_type = ? AND data_set IS ?", + selectionArgs); + } + } + } + + db.execSQL("DROP TABLE settings;"); + + // If the local account exists update it's settings so that ungrouped contacts are + // visible by default for the local account. + values.clear(); + values.put("ungrouped_visible", true); + values.put("should_sync", false); + db.update("accounts", values, + "account_name IS NULL AND account_type IS NULL AND data_set IS NULL", null); + } + + private void upgradeToVersion1601(SQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE accounts ADD x_is_default INTEGER NOT NULL DEFAULT 0;"); + } catch (SQLException ignore) { + Log.v(TAG, "Version 1601: Columns already exist, skipping upgrade steps."); + } + } + + private void upgradeToVersion1603(SQLiteDatabase db) { + try { + // Drop the view that was created in 1602 which was reverted + db.execSQL("DROP VIEW IF EXISTS view_raw_contacts_lookup_compat"); + } catch (SQLException ignore) { + Log.v(TAG, "Version 1603: failed to remove view_raw_contacts_lookup_compat."); + } + } + + @VisibleForTesting + public void upgradeToVersion1604(SQLiteDatabase db) { + // Create colums for IS_PHONE_ACCOUNT_MIGRATION_PENDING + try { + db.execSQL("ALTER TABLE data ADD is_preferred_phone_account_migration_pending" + + " INTEGER NOT NULL DEFAULT 0;"); + } catch (SQLException ignore) { + Log.v(TAG, "Version 1604: Columns already exist, skipping upgrade steps."); + } + mPhoneAccountHandleMigrationUtils.markAllTelephonyPhoneAccountsPendingMigration(db); + mPhoneAccountHandleMigrationUtils.migrateIccIdToSubId(db); + } + + protected void migrateIccIdToSubId() { + mPhoneAccountHandleMigrationUtils.migrateIccIdToSubId(getWritableDatabase()); + } + + protected void migratePendingPhoneAccountHandles(String iccId, String subId) { + mPhoneAccountHandleMigrationUtils.migratePendingPhoneAccountHandles( + iccId, subId, getWritableDatabase()); + } + + protected void updatePhoneAccountHandleMigrationPendingStatus() { + mPhoneAccountHandleMigrationUtils.updatePhoneAccountHandleMigrationPendingStatus( + getWritableDatabase()); + } + /** * This method is only used in upgradeToVersion1101 method, and should not be used in other * places now. Because data15 is not used to generate hash_id for photo, and the new generating @@ -3632,8 +3806,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { // Tiny tables updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS, null, "10"); - updateIndexStats(db, Tables.SETTINGS, - null, "10"); updateIndexStats(db, Tables.PACKAGES, null, "0"); updateIndexStats(db, Tables.DIRECTORIES, @@ -3708,7 +3880,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";"); db.execSQL("DELETE FROM " + Tables.GROUPS + ";"); db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";"); - db.execSQL("DELETE FROM " + Tables.SETTINGS + ";"); db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";"); db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX + ";"); db.execSQL("DELETE FROM " + Tables.DELETED_CONTACTS + ";"); @@ -3998,7 +4169,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } finally { insert.close(); } - return id; } @@ -4066,6 +4236,59 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } /** + * Set is_default column for the given account name and account type. + * + * @param accountName The account name to be set to default. + * @param accountType The account type to be set to default. + * @throws IllegalArgumentException if the account name or type is null. + */ + public void setDefaultAccount(String accountName, String accountType) { + if (TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("Account name or type is null."); + } + SQLiteDatabase db = getWritableDatabase(); + db.execSQL( + "UPDATE " + Tables.ACCOUNTS + + " SET " + AccountsColumns.IS_DEFAULT + "=0" + + " WHERE " + AccountsColumns.IS_DEFAULT + "=1"); + + Long accountId = getAccountIdOrNull(new AccountWithDataSet(accountName, accountType, null)); + ContentValues values = new ContentValues(); + values.put(AccountsColumns.IS_DEFAULT, 1); + if (accountId == null) { + if (!TextUtils.isEmpty(accountName)) { + values.put(AccountsColumns.ACCOUNT_NAME, accountName); + } + if (!TextUtils.isEmpty(accountType)) { + values.put(AccountsColumns.ACCOUNT_TYPE, accountType); + } + db.insert(Tables.ACCOUNTS, null, values); + } else { + db.update(Tables.ACCOUNTS, values, AccountsColumns.CONCRETE_ID + "=" + accountId, null); + } + } + + /** + * Return the default account from Accounts table. + */ + public Account getDefaultAccount() { + Account defaultAccount = null; + try (Cursor c = getReadableDatabase().rawQuery( + "SELECT " + AccountsColumns.ACCOUNT_NAME + "," + + AccountsColumns.ACCOUNT_TYPE + " FROM " + Tables.ACCOUNTS + " WHERE " + + AccountsColumns.IS_DEFAULT + " = 1", null)) { + while (c.moveToNext()) { + String accountName = c.getString(0); + String accountType = c.getString(1); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + defaultAccount = new Account(accountName, accountType); + } + } + } + return defaultAccount; + } + + /** * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts. */ public void updateAllVisible() { @@ -4117,12 +4340,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + GroupsColumns.CONCRETE_ACCOUNT_ID + " AND " + Groups.AUTO_ADD + " != 0" + ")" + - ") OR EXISTS (" + - "SELECT " + RawContacts._ID + - " FROM " + Tables.RAW_CONTACTS + - " WHERE " + RawContacts.CONTACT_ID + "=?1" + - " AND " + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + - Clauses.LOCAL_ACCOUNT_ID + ")", new String[] { contactIdAsString, @@ -5120,6 +5337,38 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } } + private static void invalidateDeviceConfigCacheIfTooOld() { + final long now = SystemClock.elapsedRealtime(); + if (sDeviceConfigCacheExpirationElapsedTime > now) { + return; + } + if (AbstractContactsProvider.VERBOSE_LOGGING) { + Log.v(TAG, "Invalidating device config cache"); + } + sSimpleFieldMaxSizeCached = null; + sDeviceConfigCacheExpirationElapsedTime = now + DEVICE_CONFIG_CACHE_EXPIRATION_MS; + } + + /** + * @return the max size for "simple" fields from the device config setting. + */ + public static int getSimpleFieldMaxSize() { + invalidateDeviceConfigCacheIfTooOld(); + final Integer cached = sSimpleFieldMaxSizeCached; + if (cached != null) { + return cached; + } + final long token = Binder.clearCallingIdentity(); + try { + final int value = DeviceConfig.getInt(DeviceConfig.NAMESPACE_CONTACTS_PROVIDER, + SIMPLE_FIELD_MAX_SIZE_KEY, SIMPLE_FIELD_MAX_SIZE_DEFAULT); + sSimpleFieldMaxSizeCached = value; + return value; + } finally { + Binder.restoreCallingIdentity(token); + } + } + public void dump(PrintWriter pw) { pw.print("CountryISO: "); pw.println(getCurrentCountryIso()); diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 0c6e8192..3f61a15a 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -20,6 +20,9 @@ import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME; + +import android.os.Looper; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; @@ -27,6 +30,7 @@ import android.annotation.Nullable; import android.annotation.WorkerThread; import android.app.AppOpsManager; import android.app.SearchManager; +import android.content.BroadcastReceiver; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; @@ -35,6 +39,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.IContentService; import android.content.Intent; +import android.content.IntentFilter; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.content.SyncAdapterType; @@ -64,7 +69,6 @@ import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; -import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.RemoteException; @@ -116,7 +120,10 @@ import android.provider.OpenableColumns; import android.provider.Settings.Global; import android.provider.SyncStateContract; import android.sysprop.ContactsProperties; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -148,12 +155,11 @@ import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Projections; import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns; -import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns; import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns; +import com.android.providers.contacts.ContactsDatabaseHelper.ViewSettingsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Views; import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder; import com.android.providers.contacts.aggregation.AbstractContactAggregator; @@ -173,6 +179,7 @@ import com.android.providers.contacts.util.DbQueryUtils; import com.android.providers.contacts.util.LogFields; import com.android.providers.contacts.util.LogUtils; import com.android.providers.contacts.util.NeededForTesting; +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import com.android.providers.contacts.util.UserUtils; import com.android.vcard.VCardComposer; import com.android.vcard.VCardConfig; @@ -203,6 +210,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -220,6 +228,8 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final String WRITE_PERMISSION = "android.permission.WRITE_CONTACTS"; private static final String MANAGE_SIM_ACCOUNTS_PERMISSION = "android.contacts.permission.MANAGE_SIM_ACCOUNTS"; + private static final String SET_DEFAULT_ACCOUNT_PERMISSION = + "android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"; /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; @@ -248,6 +258,9 @@ public class ContactsProvider2 extends AbstractContactsProvider private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10; private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11; private static final int BACKGROUND_TASK_RESCAN_DIRECTORY = 12; + private static final int BACKGROUND_TASK_CLEANUP_DANGLING_CONTACTS = 13; + @VisibleForTesting + protected static final int BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES = 14; protected static final int STATUS_NORMAL = 0; protected static final int STATUS_UPGRADING = 1; @@ -266,6 +279,9 @@ public class ContactsProvider2 extends AbstractContactsProvider /** Rate limit (in milliseconds) for photo cleanup. Do it at most once per day. */ private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; + /** Rate limit (in milliseconds) for dangling contacts cleanup. Do it at most once per day. */ + private static final int DANGLING_CONTACTS_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; + /** Maximum length of a phone number that can be inserted into the database */ private static final int PHONE_NUMBER_LENGTH_LIMIT = 1000; @@ -1011,15 +1027,10 @@ public class ContactsProvider2 extends AbstractContactsProvider + " THEN 1" + " ELSE MIN(" + Groups.SHOULD_SYNC + ")" + " END)" - + " FROM " + Views.GROUPS - + " WHERE " + ViewGroupsColumns.CONCRETE_ACCOUNT_NAME + "=" - + SettingsColumns.CONCRETE_ACCOUNT_NAME - + " AND " + ViewGroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" - + SettingsColumns.CONCRETE_ACCOUNT_TYPE - + " AND ((" + ViewGroupsColumns.CONCRETE_DATA_SET + " IS NULL AND " - + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR (" - + ViewGroupsColumns.CONCRETE_DATA_SET + "=" - + SettingsColumns.CONCRETE_DATA_SET + "))))=0" + + " FROM " + Tables.GROUPS + + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + + ViewSettingsColumns.CONCRETE_ACCOUNT_ID + + "))=0" + " THEN 1" + " ELSE 0" + " END)") @@ -1430,6 +1441,7 @@ public class ContactsProvider2 extends AbstractContactsProvider private PostalSplitter mPostalSplitter; private ContactDirectoryManager mContactDirectoryManager; + private SubscriptionManager mSubscriptionManager; private boolean mIsPhoneInitialized; private boolean mIsPhone; @@ -1467,6 +1479,8 @@ public class ContactsProvider2 extends AbstractContactsProvider private long mLastPhotoCleanup = 0; + private long mLastDanglingContactsCleanup = 0; + private FastScrollingIndexCache mFastScrollingIndexCache; // Stats about FastScrollingIndex. @@ -1477,6 +1491,36 @@ public class ContactsProvider2 extends AbstractContactsProvider // Enterprise members private EnterprisePolicyGuard mEnterprisePolicyGuard; + private Set<PhoneAccountHandle> mMigratedPhoneAccountHandles; + + /** + * Subscription change will trigger ACTION_PHONE_ACCOUNT_REGISTERED that broadcasts new + * PhoneAccountHandle that is created based on the new subscription. This receiver is used + * for listening new subscription change and migrating phone account handle if any pending. + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(intent.getAction())) { + PhoneAccountHandle phoneAccountHandle = + (PhoneAccountHandle) intent.getParcelableExtra( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); + Log.i(TAG, "onReceive ACTION_PHONE_ACCOUNT_REGISTERED pending? " + + mContactsHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending()); + if (mContactsHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending() + && TELEPHONY_COMPONENT_NAME.equals( + phoneAccountHandle.getComponentName().flattenToString()) + && !mMigratedPhoneAccountHandles.contains(phoneAccountHandle)) { + mMigratedPhoneAccountHandles.add(phoneAccountHandle); + scheduleBackgroundTask( + BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES, phoneAccountHandle); + } + } + } + }; + @Override public boolean onCreate() { if (VERBOSE_LOGGING) { @@ -1523,7 +1567,7 @@ public class ContactsProvider2 extends AbstractContactsProvider new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext()); - + mSubscriptionManager = getContext().getSystemService(SubscriptionManager.class); mContactsHelper = getDatabaseHelper(); mDbHelper.set(mContactsHelper); @@ -1533,6 +1577,12 @@ public class ContactsProvider2 extends AbstractContactsProvider mContactDirectoryManager = new ContactDirectoryManager(this); mGlobalSearchSupport = new GlobalSearchSupport(this); + if (mContactsHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending()) { + IntentFilter filter = new IntentFilter(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); + getContext().registerReceiver(mBroadcastReceiver, filter); + } + // The provider is closed for business until fully initialized mReadAccessLatch = new CountDownLatch(1); mWriteAccessLatch = new CountDownLatch(1); @@ -1552,12 +1602,14 @@ public class ContactsProvider2 extends AbstractContactsProvider mProfileProvider.attachInfo(getContext(), profileInfo); mProfileHelper = mProfileProvider.getDatabaseHelper(); mEnterprisePolicyGuard = new EnterprisePolicyGuard(getContext()); + mMigratedPhoneAccountHandles = new HashSet<>(); // Initialize the pre-authorized URI duration. mPreAuthorizedUriDuration = DEFAULT_PREAUTHORIZED_URI_EXPIRATION; scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE); scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); + scheduleBackgroundTask(BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES); scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE); scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM); scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX); @@ -1565,6 +1617,7 @@ public class ContactsProvider2 extends AbstractContactsProvider scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS); scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG); + scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_DANGLING_CONTACTS); ContactsPackageMonitor.start(getContext()); @@ -1680,6 +1733,7 @@ public class ContactsProvider2 extends AbstractContactsProvider switchToContactMode(); switch (task) { case BACKGROUND_TASK_INITIALIZE: { + mContactsHelper.updatePhoneAccountHandleMigrationPendingStatus(); initForDefaultLocale(); mReadAccessLatch.countDown(); mReadAccessLatch = null; @@ -1715,6 +1769,32 @@ public class ContactsProvider2 extends AbstractContactsProvider break; } + case BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES: { + if (arg == null) { + // No phone account handle specified, try to execute all pending migrations. + if (mContactsHelper.getPhoneAccountHandleMigrationUtils() + .isPhoneAccountMigrationPending()) { + mContactsHelper.migrateIccIdToSubId(); + } + } else { + // Phone account handle specified, task scheduled when + // ACTION_PHONE_ACCOUNT_REGISTERED received. + PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) arg; + String iccId = mSubscriptionManager.getActiveSubscriptionInfo( + Integer.parseInt(phoneAccountHandle.getId())).getIccId(); + if (iccId == null) { + Log.i(TAG, "ACTION_PHONE_ACCOUNT_REGISTERED received null IccId."); + } else { + Log.i(TAG, "ACTION_PHONE_ACCOUNT_REGISTERED received for migrating phone" + + " account handle SubId: " + phoneAccountHandle.getId()); + mContactsHelper.migratePendingPhoneAccountHandles(iccId, + phoneAccountHandle.getId()); + mContactsHelper.updatePhoneAccountHandleMigrationPendingStatus(); + } + } + break; + } + case BACKGROUND_TASK_RESCAN_DIRECTORY: { updateDirectoriesInBackground(true); break; @@ -1770,6 +1850,17 @@ public class ContactsProvider2 extends AbstractContactsProvider DeletedContactsTableUtil.deleteOldLogs(db); break; } + + case BACKGROUND_TASK_CLEANUP_DANGLING_CONTACTS: { + // Check rate limit. + long now = System.currentTimeMillis(); + if (now - mLastDanglingContactsCleanup > DANGLING_CONTACTS_CLEANUP_RATE_LIMIT) { + mLastDanglingContactsCleanup = now; + + cleanupDanglingContacts(); + } + break; + } } } @@ -1997,6 +2088,31 @@ public class ContactsProvider2 extends AbstractContactsProvider } } + @VisibleForTesting + protected void cleanupDanglingContacts() { + // Dangling contacts are the contacts whose _id doesn't have a raw_contact_id linked with. + String danglingContactsSelection = + Contacts._ID + + " NOT IN (SELECT " + + RawContacts.CONTACT_ID + + " FROM " + + Tables.RAW_CONTACTS + + " WHERE " + + RawContacts.DELETED + + " = 0)"; + int danglingContactsCount = + mDbHelper + .get() + .getWritableDatabase() + .delete(Tables.CONTACTS, danglingContactsSelection, /* selectionArgs= */ null); + LogFields.Builder logBuilder = + LogFields.Builder.aLogFields() + .setTaskType(LogUtils.TaskType.DANGLING_CONTACTS_CLEANUP_TASK) + .setResultCount(danglingContactsCount); + LogUtils.log(logBuilder.build()); + Log.v(TAG, danglingContactsCount + " Dangling Contacts have been cleaned up."); + } + @Override public ContactsDatabaseHelper newDatabaseHelper(final Context context) { return ContactsDatabaseHelper.getInstance(context); @@ -2272,10 +2388,6 @@ public class ContactsProvider2 extends AbstractContactsProvider @Override public Bundle call(String method, String arg, Bundle extras) { - LogFields.Builder logBuilder = - LogFields.Builder.aLogFields() - .setApiType(LogUtils.ApiType.CALL) - .setStartNanos(SystemClock.elapsedRealtimeNanos()); waitForAccess(mReadAccessLatch); switchToContactMode(); if (Authorization.AUTHORIZATION_METHOD.equals(method)) { @@ -2316,54 +2428,34 @@ public class ContactsProvider2 extends AbstractContactsProvider throw new IllegalArgumentException("Account name or type is empty"); } - long resultId = -1; final Bundle response = new Bundle(); final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); db.beginTransaction(); try { - resultId = mDbHelper.get().createSimAccountIdInTransaction( + mDbHelper.get().createSimAccountIdInTransaction( AccountWithDataSet.get(accountName, accountType, null), simSlot, efType); db.setTransactionSuccessful(); - } catch (Exception e) { - logBuilder.setException(e); - throw e; } finally { - LogUtils.log( - logBuilder - .setMethodCall(LogUtils.MethodCall.ADD_SIM_ACCOUNTS) - .setResultCount(resultId > -1 ? 1 : 0) - .build()); db.endTransaction(); } - getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); return response; } else if (SimContacts.REMOVE_SIM_ACCOUNT_METHOD.equals(method)) { - ContactsPermissions.enforceCallingOrSelfPermission( - getContext(), MANAGE_SIM_ACCOUNTS_PERMISSION); + ContactsPermissions.enforceCallingOrSelfPermission(getContext(), + MANAGE_SIM_ACCOUNTS_PERMISSION); final int simSlot = extras.getInt(SimContacts.KEY_SIM_SLOT_INDEX, -1); if (simSlot < 0) { throw new IllegalArgumentException("Sim slot is negative"); } - - int removedCount = 0; final Bundle response = new Bundle(); final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); db.beginTransaction(); try { - removedCount = mDbHelper.get().removeSimAccounts(simSlot); + mDbHelper.get().removeSimAccounts(simSlot); scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); db.setTransactionSuccessful(); - } catch (Exception e) { - logBuilder.setException(e); - throw e; } finally { - LogUtils.log( - logBuilder - .setMethodCall(LogUtils.MethodCall.REMOVE_SIM_ACCOUNTS) - .setResultCount(removedCount) - .build()); db.endTransaction(); } getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); @@ -2371,26 +2463,64 @@ public class ContactsProvider2 extends AbstractContactsProvider } else if (SimContacts.QUERY_SIM_ACCOUNTS_METHOD.equals(method)) { ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION); final Bundle response = new Bundle(); - int accountsCount = 0; - try { - final List<SimAccount> simAccounts = mDbHelper.get().getAllSimAccounts(); - response.putParcelableList(SimContacts.KEY_SIM_ACCOUNTS, simAccounts); - accountsCount = simAccounts.size(); - return response; - } catch (Exception e) { - logBuilder.setException(e); - throw e; - } finally { - LogUtils.log( - logBuilder - .setMethodCall(LogUtils.MethodCall.GET_SIM_ACCOUNTS) - .setResultCount(accountsCount) - .build()); - } + + final List<SimAccount> simAccounts = mDbHelper.get().getAllSimAccounts(); + response.putParcelableList(SimContacts.KEY_SIM_ACCOUNTS, simAccounts); + + return response; + } else if (Settings.QUERY_DEFAULT_ACCOUNT_METHOD.equals(method)) { + ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION); + final Bundle response = new Bundle(); + + final Account defaultAccount = mDbHelper.get().getDefaultAccount(); + response.putParcelable(Settings.KEY_DEFAULT_ACCOUNT, defaultAccount); + + return response; + } else if (Settings.SET_DEFAULT_ACCOUNT_METHOD.equals(method)) { + return setDefaultAccountSetting(extras); } return null; } + private Bundle setDefaultAccountSetting(Bundle extras) { + ContactsPermissions.enforceCallingOrSelfPermission(getContext(), + SET_DEFAULT_ACCOUNT_PERMISSION); + final String accountName = extras.getString(Settings.ACCOUNT_NAME); + final String accountType = extras.getString(Settings.ACCOUNT_TYPE); + final String dataSet = extras.getString(Settings.DATA_SET); + + if (TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException( + "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE"); + } + if (!TextUtils.isEmpty(dataSet)) { + throw new IllegalArgumentException( + "Cannot set default account with non-null data set."); + } + + AccountWithDataSet accountWithDataSet = new AccountWithDataSet( + accountName, accountType, dataSet); + Account[] systemAccounts = AccountManager.get(getContext()).getAccounts(); + List<SimAccount> simAccounts = mDbHelper.get().getAllSimAccounts(); + if (!accountWithDataSet.isLocalAccount() + && !accountWithDataSet.inSystemAccounts(systemAccounts) + && !accountWithDataSet.inSimAccounts(simAccounts)) { + throw new IllegalArgumentException( + "Cannot set default account for invalid accounts."); + } + + final Bundle response = new Bundle(); + final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); + db.beginTransaction(); + try { + mDbHelper.get().setDefaultAccount(accountName, accountType); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + return response; + } + /** * Pre-authorizes the given URI, adding an expiring permission token to it and placing that * in our map of pre-authorized URIs. @@ -2763,9 +2893,9 @@ public class ContactsProvider2 extends AbstractContactsProvider } case SETTINGS: { - id = insertSettings(values); mSyncToNetwork |= !callerIsSyncAdapter; - break; + // Settings rows are referenced by the account instead of their ID. + return insertSettings(uri, values); } case STATUS_UPDATES: @@ -3323,55 +3453,35 @@ public class ContactsProvider2 extends AbstractContactsProvider return groupId; } - private long insertSettings(ContentValues values) { - // Before inserting, ensure that no settings record already exists for the - // values being inserted (this used to be enforced by a primary key, but that no - // longer works with the nullable data_set field added). - String accountName = values.getAsString(Settings.ACCOUNT_NAME); - String accountType = values.getAsString(Settings.ACCOUNT_TYPE); - String dataSet = values.getAsString(Settings.DATA_SET); - Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon(); - if (accountName != null) { - settingsUri.appendQueryParameter(Settings.ACCOUNT_NAME, accountName); - } - if (accountType != null) { - settingsUri.appendQueryParameter(Settings.ACCOUNT_TYPE, accountType); - } - if (dataSet != null) { - settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet); - } - Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0, null); - try { - if (c.getCount() > 0) { - // If a record was found, replace it with the new values. - String selection = null; - String[] selectionArgs = null; - if (accountName != null && accountType != null) { - selection = Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE + "=?"; - if (dataSet == null) { - selection += " AND " + Settings.DATA_SET + " IS NULL"; - selectionArgs = new String[] {accountName, accountType}; - } else { - selection += " AND " + Settings.DATA_SET + "=?"; - selectionArgs = new String[] {accountName, accountType, dataSet}; - } - } - return updateSettings(values, selection, selectionArgs); - } - } finally { - c.close(); - } + private Uri insertSettings(Uri uri, ContentValues values) { + final AccountWithDataSet account = resolveAccountWithDataSet(uri, values); + // Note that the following check means the local account settings cannot be created with + // an insert because resolveAccountWithDataSet returns null for it. However, the settings + // for it can be updated once it is created automatically by a raw contact or group insert. + if (account == null) { + return null; + } + final ContactsDatabaseHelper dbHelper = mDbHelper.get(); final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); - // If we didn't find a duplicate, we're fine to insert. - final long id = db.insert(Tables.SETTINGS, null, values); + long accountId = dbHelper.getOrCreateAccountIdInTransaction(account); + mSelectionArgs1[0] = String.valueOf(accountId); + + int count = db.update(Views.SETTINGS, values, + ViewSettingsColumns.ACCOUNT_ID + "= ?", mSelectionArgs1); if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { mVisibleTouched = true; } - return id; + Uri.Builder builder = Settings.CONTENT_URI.buildUpon() + .appendQueryParameter(Settings.ACCOUNT_NAME, account.getAccountName()) + .appendQueryParameter(Settings.ACCOUNT_TYPE, account.getAccountType()); + if (account.getDataSet() != null) { + builder.appendQueryParameter(Settings.DATA_SET, account.getDataSet()); + } + return builder.build(); } /** @@ -3798,7 +3908,7 @@ public class ContactsProvider2 extends AbstractContactsProvider case SETTINGS: { mSyncToNetwork |= !callerIsSyncAdapter; - return deleteSettings(appendAccountToSelection(uri, selection), selectionArgs); + return deleteSettings(appendAccountIdToSelection(uri, selection), selectionArgs); } case STATUS_UPDATES: @@ -3875,9 +3985,21 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - private int deleteSettings(String selection, String[] selectionArgs) { + private int deleteSettings(String initialSelection, String[] selectionArgs) { final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); - final int count = db.delete(Tables.SETTINGS, selection, selectionArgs); + + int count = 0; + final String selection = DbQueryUtils.concatenateClauses( + initialSelection, Clauses.DELETABLE_SETTINGS); + try (Cursor cursor = db.query(Views.SETTINGS, + new String[] { ViewSettingsColumns.ACCOUNT_ID }, + selection, selectionArgs, null, null, null)) { + while (cursor.moveToNext()) { + mSelectionArgs1[0] = cursor.getString(0); + db.delete(Tables.ACCOUNTS, AccountsColumns._ID + "=?", mSelectionArgs1); + count++; + } + } mVisibleTouched = true; return count; } @@ -4546,7 +4668,20 @@ public class ContactsProvider2 extends AbstractContactsProvider private int updateSettings(ContentValues values, String selection, String[] selectionArgs) { final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); - final int count = db.update(Tables.SETTINGS, values, selection, selectionArgs); + + int count = 0; + // We have to query for the count because the update is using a trigger and triggers + // don't return a count of modified rows. + try (Cursor cursor = db.query(Views.SETTINGS, + new String[] { "COUNT(*)" }, + selection, selectionArgs, null, null, null)) { + if (cursor.moveToFirst()) { + count = cursor.getInt(0); + } + } + if (count > 0) { + db.update(Views.SETTINGS, values, selection, selectionArgs); + } if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { mVisibleTouched = true; } @@ -5327,9 +5462,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - // Second, remove stale rows from Tables.SETTINGS and Tables.DIRECTORIES - removeStaleAccountRows( - Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE, systemAccounts); + // Second, remove stale rows from Tables.DIRECTORIES removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME, Directory.ACCOUNT_TYPE, systemAccounts); @@ -5644,8 +5777,8 @@ public class ContactsProvider2 extends AbstractContactsProvider Log.v(TAG, "Making authority " + directoryAuthority + " visible to UID " + callingUid); } - getContext().getPackageManager().grantImplicitAccess( - callingUid, directoryAuthority); + getContext().getPackageManager() + .makeProviderVisible(callingUid, directoryAuthority); } // Load the cursor contents into a memory cursor (backed by a cursor window) and close the @@ -6911,9 +7044,9 @@ public class ContactsProvider2 extends AbstractContactsProvider } case SETTINGS: { - qb.setTables(Tables.SETTINGS); + qb.setTables(Views.SETTINGS); qb.setProjectionMap(sSettingsProjectionMap); - appendAccountFromParameter(qb, uri); + appendAccountIdFromParameter(qb, uri); // When requesting specific columns, this query requires // late-binding of the GroupMembership MIME-type. @@ -9869,6 +10002,15 @@ public class ContactsProvider2 extends AbstractContactsProvider return mDbHelper.get(); } + /** + * @return the currently registered BroadcastReceiver for listening + * ACTION_PHONE_ACCOUNT_REGISTERED in the current process. + */ + @NeededForTesting + public BroadcastReceiver getBroadcastReceiverForTest() { + return mBroadcastReceiver; + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContactAggregator != null) { @@ -9955,6 +10097,12 @@ public class ContactsProvider2 extends AbstractContactsProvider return mContactsHelper; } + /** Should be only used in tests. */ + @NeededForTesting + public void setContactsDatabaseHelperForTest(ContactsDatabaseHelper contactsHelper) { + mContactsHelper = contactsHelper; + } + @VisibleForTesting public ProfileProvider getProfileProviderForTest() { return mProfileProvider; diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java index a82ce34f..b1295c1f 100644 --- a/src/com/android/providers/contacts/DataRowHandler.java +++ b/src/com/android/providers/contacts/DataRowHandler.java @@ -27,6 +27,9 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.Data; import android.text.TextUtils; +import android.util.Log; +import android.util.LogWriter; + import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; @@ -37,6 +40,7 @@ import com.android.providers.contacts.aggregation.AbstractContactAggregator; * Handles inserts and update for a specific Data type. */ public abstract class DataRowHandler { + private static final String TAG = AbstractContactsProvider.TAG; private static final String[] HASH_INPUT_COLUMNS = new String[] { Data.DATA1, Data.DATA2}; @@ -439,4 +443,14 @@ public abstract class DataRowHandler { } return false; } + + protected static void applySimpleFieldMaxSize(ContentValues cv, String column) { + final int maxSize = ContactsDatabaseHelper.getSimpleFieldMaxSize(); + String v = cv.getAsString(column); + if (v == null || v.length() <= maxSize) { + return; + } + Log.w(TAG, "Truncating field " + column + ": length=" + v.length() + " max=" + maxSize); + cv.put(column, v.substring(0, maxSize)); + } } diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java index 539c9596..76196df3 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java +++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java @@ -33,9 +33,15 @@ public class DataRowHandlerForEmail extends DataRowHandlerForCommonDataKind { super(context, dbHelper, aggregator, Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); } + private void applySimpleFieldMaxSize(ContentValues cv) { + applySimpleFieldMaxSize(cv, Email.DATA); + } + @Override public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values) { + applySimpleFieldMaxSize(values); + String email = values.getAsString(Email.DATA); long dataId = super.insert(db, txContext, rawContactId, values); @@ -51,6 +57,7 @@ public class DataRowHandlerForEmail extends DataRowHandlerForCommonDataKind { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { + applySimpleFieldMaxSize(values); if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java index 052252e1..85e76581 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java +++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java @@ -19,6 +19,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; @@ -38,9 +39,15 @@ public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKin super(context, dbHelper, aggregator, Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); } + private void applySimpleFieldMaxSize(ContentValues cv) { + applySimpleFieldMaxSize(cv, Phone.NUMBER); + applySimpleFieldMaxSize(cv, Phone.NORMALIZED_NUMBER); + } + @Override public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values) { + applySimpleFieldMaxSize(values); fillNormalizedNumber(values); final long dataId = super.insert(db, txContext, rawContactId, values); @@ -59,6 +66,7 @@ public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKin @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { + applySimpleFieldMaxSize(values); fillNormalizedNumber(values); if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java index 044e9726..11c24a7e 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java +++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java @@ -19,6 +19,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.FullNameStyle; import android.provider.ContactsContract.PhoneticNameStyle; @@ -43,9 +44,23 @@ public class DataRowHandlerForStructuredName extends DataRowHandler { mNameLookupBuilder = nameLookupBuilder; } + private void applySimpleFieldMaxSize(ContentValues cv) { + applySimpleFieldMaxSize(cv, StructuredName.DISPLAY_NAME); + applySimpleFieldMaxSize(cv, StructuredName.GIVEN_NAME); + applySimpleFieldMaxSize(cv, StructuredName.FAMILY_NAME); + applySimpleFieldMaxSize(cv, StructuredName.PREFIX); + applySimpleFieldMaxSize(cv, StructuredName.MIDDLE_NAME); + applySimpleFieldMaxSize(cv, StructuredName.SUFFIX); + + applySimpleFieldMaxSize(cv, StructuredName.PHONETIC_GIVEN_NAME); + applySimpleFieldMaxSize(cv, StructuredName.PHONETIC_MIDDLE_NAME); + applySimpleFieldMaxSize(cv, StructuredName.PHONETIC_FAMILY_NAME); + } + @Override public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values) { + applySimpleFieldMaxSize(values); fixStructuredNameComponents(values, values); long dataId = super.insert(db, txContext, rawContactId, values); @@ -64,6 +79,7 @@ public class DataRowHandlerForStructuredName extends DataRowHandler { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { + applySimpleFieldMaxSize(values); final long dataId = c.getLong(DataUpdateQuery._ID); final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java index c111cf39..3120fae2 100644 --- a/src/com/android/providers/contacts/LegacyApiSupport.java +++ b/src/com/android/providers/contacts/LegacyApiSupport.java @@ -65,6 +65,8 @@ import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; +import com.android.providers.contacts.ContactsDatabaseHelper.Views; +import com.android.providers.contacts.database.MoreDatabaseUtils; import java.util.Locale; @@ -1246,7 +1248,7 @@ public class LegacyApiSupport { + ContactsContract.Settings.ACCOUNT_NAME + "," + ContactsContract.Settings.ACCOUNT_TYPE + "," + ContactsContract.Settings.SHOULD_SYNC + - " FROM " + Tables.SETTINGS + " LEFT OUTER JOIN " + LegacyTables.SETTINGS + + " FROM " + Views.SETTINGS + " LEFT OUTER JOIN " + LegacyTables.SETTINGS + " ON (" + ContactsContract.Settings.ACCOUNT_NAME + "=" + android.provider.Contacts.Settings._SYNC_ACCOUNT + " AND " + ContactsContract.Settings.ACCOUNT_TYPE + "=" @@ -1859,8 +1861,12 @@ public class LegacyApiSupport { sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "="); DatabaseUtils.appendEscapedSQLString(sb, mAccount.type); } else { - sb.append(RawContacts.ACCOUNT_NAME + " IS NULL" + - " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"); + sb.append(RawContacts.ACCOUNT_NAME + " IS "); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull( + sb, AccountWithDataSet.LOCAL.getAccountName()); + sb.append(" AND ").append(RawContacts.ACCOUNT_TYPE + " IS "); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull( + sb, AccountWithDataSet.LOCAL.getAccountType()); } } @@ -1877,8 +1883,12 @@ public class LegacyApiSupport { sb.append(" AND " + Groups.ACCOUNT_TYPE + "="); DatabaseUtils.appendEscapedSQLString(sb, mAccount.type); } else { - sb.append(Groups.ACCOUNT_NAME + " IS NULL" + - " AND " + Groups.ACCOUNT_TYPE + " IS NULL"); + sb.append(Groups.ACCOUNT_NAME + " IS "); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull( + sb, AccountWithDataSet.LOCAL.getAccountName()); + sb.append(" AND " + Groups.ACCOUNT_TYPE + " IS "); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull( + sb, AccountWithDataSet.LOCAL.getAccountType()); } } diff --git a/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java b/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java deleted file mode 100644 index 8a688892..00000000 --- a/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.providers.contacts; - -import android.content.BroadcastReceiver; -import android.content.ContentProvider; -import android.content.Context; -import android.content.IContentProvider; -import android.content.Intent; -import android.provider.CallLog; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; - -/** - * This will be launched when a new phone account is registered in telecom. It is used by the call - * log to un-hide any entries which were previously hidden after a backup-restore until it's - * associated phone-account is registered with telecom. - * - * IOW, after a restore, we hide call log entries until the user inserts the corresponding SIM, - * registers the corresponding SIP account, or registers a corresponding alternative phone-account. - */ -public class PhoneAccountRegistrationReceiver extends BroadcastReceiver { - static final String TAG = "PhoneAccountReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - // We are now running with the system up, but no apps started, - // so can do whatever cleanup after an upgrade that we want. - if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(intent.getAction())) { - - PhoneAccountHandle handle = (PhoneAccountHandle) intent.getParcelableExtra( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); - - IContentProvider iprovider = - context.getContentResolver().acquireProvider(CallLog.AUTHORITY); - ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider); - if (provider instanceof CallLogProvider) { - ((CallLogProvider) provider).adjustForNewPhoneAccount(handle); - } - } - } -} diff --git a/src/com/android/providers/contacts/database/MoreDatabaseUtils.java b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java index 3dadb3fb..61ffb9f1 100644 --- a/src/com/android/providers/contacts/database/MoreDatabaseUtils.java +++ b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java @@ -16,11 +16,13 @@ package com.android.providers.contacts.database; -import com.android.providers.contacts.util.NeededForTesting; - +import android.annotation.Nullable; import android.database.Cursor; +import android.database.DatabaseUtils; import android.util.Log; +import com.android.providers.contacts.util.NeededForTesting; + /** * Static methods for database operations. */ @@ -108,4 +110,30 @@ public class MoreDatabaseUtils { Log.d(logTag, sb.toString()); } } + + /** + * Same as {@link DatabaseUtils#sqlEscapeString(String)} but handles a null argument by + * returning the string "NULL". + * + * @return the SQL-escaped string or "NULL" if the argument is null. + */ + @Nullable + public static String sqlEscapeNullableString(@Nullable String s) { + return s == null + ? "NULL" + : DatabaseUtils.sqlEscapeString(s); + } + + /** + * Same as {@link DatabaseUtils#appendEscapedSQLString(StringBuilder, String)} but handles a + * null argument by appending the literal string "NULL". + */ + @Nullable + public static void appendEscapedSQLStringOrLiteralNull(StringBuilder sb, @Nullable String s) { + if (s == null) { + sb.append("NULL"); + } else { + DatabaseUtils.appendEscapedSQLString(sb, s); + } + } } diff --git a/src/com/android/providers/contacts/util/LogFields.java b/src/com/android/providers/contacts/util/LogFields.java index 4d07ca4b..fc05c847 100644 --- a/src/com/android/providers/contacts/util/LogFields.java +++ b/src/com/android/providers/contacts/util/LogFields.java @@ -24,6 +24,9 @@ public final class LogFields { private final int uriType; + // The type is from LogUtils.TaskType + private final int taskType; + private final boolean callerIsSyncAdapter; private final long startNanos; @@ -34,11 +37,11 @@ public final class LogFields { private int resultCount; - private int methodCall; - - public LogFields(int apiType, int uriType, boolean callerIsSyncAdapter, long startNanos) { + public LogFields( + int apiType, int uriType, int taskType, boolean callerIsSyncAdapter, long startNanos) { this.apiType = apiType; this.uriType = uriType; + this.taskType = taskType; this.callerIsSyncAdapter = callerIsSyncAdapter; this.startNanos = startNanos; } @@ -51,6 +54,10 @@ public final class LogFields { return uriType; } + public int getTaskType() { + return taskType; + } + public boolean isCallerIsSyncAdapter() { return callerIsSyncAdapter; } @@ -71,19 +78,15 @@ public final class LogFields { return resultCount; } - public int getMethodCall() { - return methodCall; - } - public static final class Builder { private int apiType; private int uriType; + private int taskType; private boolean callerIsSyncAdapter; private long startNanos; private Exception exception; private Uri resultUri; private int resultCount; - private int methodCall; private Builder() { } @@ -102,6 +105,11 @@ public final class LogFields { return this; } + public Builder setTaskType(int taskType) { + this.taskType = taskType; + return this; + } + public Builder setCallerIsSyncAdapter(boolean callerIsSyncAdapter) { this.callerIsSyncAdapter = callerIsSyncAdapter; return this; @@ -127,17 +135,12 @@ public final class LogFields { return this; } - public Builder setMethodCall(int methodCall) { - this.methodCall = methodCall; - return this; - } - public LogFields build() { - LogFields logFields = new LogFields(apiType, uriType, callerIsSyncAdapter, startNanos); + LogFields logFields = + new LogFields(apiType, uriType, taskType, callerIsSyncAdapter, startNanos); logFields.resultCount = this.resultCount; logFields.exception = this.exception; logFields.resultUri = this.resultUri; - logFields.methodCall = this.methodCall; return logFields; } } diff --git a/src/com/android/providers/contacts/util/LogUtils.java b/src/com/android/providers/contacts/util/LogUtils.java index a564a359..23e2b140 100644 --- a/src/com/android/providers/contacts/util/LogUtils.java +++ b/src/com/android/providers/contacts/util/LogUtils.java @@ -37,7 +37,12 @@ public class LogUtils { int INSERT = 2; int UPDATE = 3; int DELETE = 4; - int CALL = 5; + } + + // Keep in sync with ContactsProviderStatus#TaskType in + // frameworks/proto_logging/stats/atoms.proto file. + public interface TaskType { + int DANGLING_CONTACTS_CLEANUP_TASK = 1; } // Keep in sync with ContactsProviderStatus#CallerType in @@ -47,16 +52,11 @@ public class LogUtils { int CALLER_IS_NOT_SYNC_ADAPTER = 2; } - // Keep in sync with ContactsProviderStatus#MethodCall in - // frameworks/proto_logging/stats/atoms.proto file. - public interface MethodCall { - int ADD_SIM_ACCOUNTS = 1; - int REMOVE_SIM_ACCOUNTS = 2; - int GET_SIM_ACCOUNTS = 3; - } - private static final int STATSD_LOG_ATOM_ID = 301; + + // The write methods must be called in the same order as the order of fields in the + // atom (frameworks/proto_logging/stats/atoms.proto) definition. public static void log(LogFields logFields) { StatsLog.write(StatsEvent.newBuilder() .setAtomId(STATSD_LOG_ATOM_ID) @@ -66,8 +66,7 @@ public class LogUtils { .writeInt(getResultType(logFields.getException())) .writeInt(logFields.getResultCount()) .writeLong(getLatencyMicros(logFields.getStartNanos())) - .writeInt(0) // Empty value for TaskType - .writeInt(logFields.getMethodCall()) + .writeInt(logFields.getTaskType()) .usePooledBuffer() .build()); } diff --git a/src/com/android/providers/contacts/util/PhoneAccountHandleMigrationUtils.java b/src/com/android/providers/contacts/util/PhoneAccountHandleMigrationUtils.java new file mode 100644 index 00000000..b578314a --- /dev/null +++ b/src/com/android/providers/contacts/util/PhoneAccountHandleMigrationUtils.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.providers.contacts.util; + +import com.android.providers.contacts.CallLogDatabaseHelper; +import com.android.providers.contacts.ContactsDatabaseHelper; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.database.sqlite.SQLiteDatabase; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.provider.ContactsContract.Data; +import android.preference.PreferenceManager; +import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionInfo; +import android.text.TextUtils; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utils for PhoneAccountHandle Migration Operations in database providers. + * + * When the database is created and upgraded, PhoneAccountHandleMigrationUtils helps migrate IccId + * to SubId. If the PhoneAccount haven't registered yet, we set the pending status for further + * migration. Databases will listen to broadcast + * {@link android.telecom.TelecomManager#ACTION_PHONE_ACCOUNT_REGISTERED} to identify a new sim + * event and performing migration for pending status if possible. + */ +public class PhoneAccountHandleMigrationUtils { + /** + * Indicates type of ContactsDatabase. + */ + public static final int TYPE_CONTACTS = 0; + /** + * Indicates type of CallLogDatabase. + */ + public static final int TYPE_CALL_LOG = 1; + + public static final String TELEPHONY_COMPONENT_NAME = + "com.android.phone/com.android.services.telephony.TelephonyConnectionService"; + private static final String[] TAGS = { + "PhoneAccountHandleMigrationUtils_ContactsDatabaseHelper", + "PhoneAccountHandleMigrationUtils_CallLogDatabaseHelper"}; + private static final String[] TABLES = {ContactsDatabaseHelper.Tables.DATA, + CallLogDatabaseHelper.Tables.CALLS}; + private static final String[] IDS = {Data._ID, Calls._ID}; + private static final String[] PENDING_STATUS_FIELDS = { + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING}; + private static final String[] COMPONENT_NAME_FIELDS = { + Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME}; + private static final String[] PHONE_ACCOUNT_ID_FIELDS = { + Data.PREFERRED_PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID}; + + private int mType; + private SubscriptionManager mSubscriptionManager; + private SharedPreferences mSharedPreferences; + + /** + * Constructor of the util. + */ + public PhoneAccountHandleMigrationUtils(Context context, int type) { + mType = type; + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + } + + /** + * Mark all the telephony phone account handles as pending migration. + */ + public void markAllTelephonyPhoneAccountsPendingMigration(SQLiteDatabase db) { + ContentValues valuesForTelephonyPending = new ContentValues(); + valuesForTelephonyPending.put(PENDING_STATUS_FIELDS[mType], 1); + String selection = COMPONENT_NAME_FIELDS[mType] + " = ?"; + String[] selectionArgs = {TELEPHONY_COMPONENT_NAME}; + db.beginTransaction(); + try { + int count = db.update( + TABLES[mType], valuesForTelephonyPending, selection, selectionArgs); + Log.i(TAGS[mType], "markAllTelephonyPhoneAccountsPendingMigration count: " + count); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Set phone account migration pending status, indicating if there is any phone account handle + * that need to migrate. Store the value in the SharedPreference to prevent the need to query + * the database in the future for pending migration. + */ + public void setPhoneAccountMigrationStatusPending(boolean status) { + mSharedPreferences.edit().putBoolean(PENDING_STATUS_FIELDS[mType], status).apply(); + } + + /** + * Checks phone account migration pending status, indicating if there is any phone account + * handle that need to migrate. Query the value in the SharedPreference to prevent the need + * to query the database in the future for pending migration. + */ + public boolean isPhoneAccountMigrationPending() { + return mSharedPreferences.getBoolean(PENDING_STATUS_FIELDS[mType], false); + } + + /** + * Updates phone account migration pending status, indicating if there is any phone account + * handle that need to migrate. + */ + public void updatePhoneAccountHandleMigrationPendingStatus(SQLiteDatabase sqLiteDatabase) { + // Check to see if any entries need phone account migration pending. + long count = DatabaseUtils.longForQuery(sqLiteDatabase, "SELECT COUNT(DISTINCT " + + IDS[mType] + ") FROM " + TABLES[mType] + " WHERE " + + PENDING_STATUS_FIELDS[mType] + " == 1", null); + if (count > 0) { + Log.i(TAGS[mType], "updatePhoneAccountHandleMigrationPendingStatus true"); + setPhoneAccountMigrationStatusPending(true); + } else { + Log.i(TAGS[mType], "updatePhoneAccountHandleMigrationPendingStatus false"); + setPhoneAccountMigrationStatusPending(false); + } + } + + /** + * Migrate all the pending phone account handles based on the given iccId and subId. + */ + public void migratePendingPhoneAccountHandles(String iccId, String subId, SQLiteDatabase db) { + ContentValues valuesForPhoneAccountId = new ContentValues(); + valuesForPhoneAccountId.put(PHONE_ACCOUNT_ID_FIELDS[mType], subId); + valuesForPhoneAccountId.put(PENDING_STATUS_FIELDS[mType], 0); + String selection = PHONE_ACCOUNT_ID_FIELDS[mType] + " LIKE ? AND " + + PENDING_STATUS_FIELDS[mType] + " = ?"; + String[] selectionArgs = {iccId, "1"}; + db.beginTransaction(); + try { + int count = db.update(TABLES[mType], valuesForPhoneAccountId, selection, selectionArgs); + Log.i(TAGS[mType], "migrated pending PhoneAccountHandle subId: " + subId + + " count: " + count); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + updatePhoneAccountHandleMigrationPendingStatus(db); + } + + /** + * Try to migrate any PhoneAccountId to SubId from IccId. + */ + public void migrateIccIdToSubId(SQLiteDatabase db) { + final HashMap<String, String> phoneAccountIdsMigrateNow = new HashMap<>(); + final Cursor phoneAccountIdsCursor = db.rawQuery( + "SELECT DISTINCT " + PHONE_ACCOUNT_ID_FIELDS[mType] + " FROM " + TABLES[mType] + + " WHERE " + PENDING_STATUS_FIELDS[mType] + " = 1", null); + + try { + List<SubscriptionInfo> subscriptionInfoList = mSubscriptionManager + .getAllSubscriptionInfoList(); + phoneAccountIdsCursor.moveToPosition(-1); + while (phoneAccountIdsCursor.moveToNext()) { + if (phoneAccountIdsCursor.isNull(0)) { + continue; + } + final String iccId = phoneAccountIdsCursor.getString(0); + String subId = null; + if (mSubscriptionManager != null) { + subId = getSubIdForIccId(iccId, subscriptionInfoList); + } + + if (!TextUtils.isEmpty(iccId)) { + if (subId != null) { + // If there is already a subId that maps to the corresponding iccid + // from an old phone account handle, migrate to the new phone account + // handle with sub id without pending. + phoneAccountIdsMigrateNow.put(iccId, subId); + Log.i(TAGS[mType], "migrateIccIdToSubId(db): found subId: " + subId); + } + } + } + } finally { + phoneAccountIdsCursor.close(); + } + // Migrate to the new phone account handle with its sub ID that is already available. + for (Map.Entry<String, String> set : phoneAccountIdsMigrateNow.entrySet()) { + migratePendingPhoneAccountHandles(set.getKey(), set.getValue(), db); + } + } + + // Return a subId that maps to the given iccId, or null if the subId is not available. + private String getSubIdForIccId(String iccId, List<SubscriptionInfo> subscriptionInfoList) { + for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) { + // Some old version callog would store phone account handle id with the IccId + // string plus "F", and the getIccId() returns IccId string itself without "F", + // so here need to use "startsWith" to match. + if (iccId.startsWith(subscriptionInfo.getIccId())) { + Log.i(TAGS[mType], "getSubIdForIccId: Found subscription ID to migrate: " + + subscriptionInfo.getSubscriptionId()); + return Integer.toString(subscriptionInfo.getSubscriptionId()); + } + } + return null; + } +}
\ No newline at end of file diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java index 3edbb458..31ea41aa 100644 --- a/src/com/android/providers/contacts/util/UserUtils.java +++ b/src/com/android/providers/contacts/util/UserUtils.java @@ -20,7 +20,6 @@ import com.android.providers.contacts.ContactsProvider2; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.UserInfo; -import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -41,7 +40,7 @@ public final class UserUtils { } public static int getCurrentUserHandle(Context context) { - return getUserManager(context).getUserHandle(); + return getUserManager(context).getProcessUserId(); } /** @@ -52,7 +51,7 @@ public final class UserUtils { */ private static UserInfo getCorpUserInfo(Context context) { final UserManager um = getUserManager(context); - final int myUser = um.getUserHandle(); + final int myUser = um.getProcessUserId(); // Check each user. for (UserInfo ui : um.getUsers()) { diff --git a/test_common/Android.bp b/test_common/Android.bp index 207b1db4..1ec6b9a9 100644 --- a/test_common/Android.bp +++ b/test_common/Android.bp @@ -14,13 +14,7 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "packages_providers_ContactsProvider_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: [ - "packages_providers_ContactsProvider_license", - ], + default_applicable_licenses: ["Android-Apache-2.0"], } java_library { diff --git a/test_common/src/com/android/providers/contacts/testutil/TestUtil.java b/test_common/src/com/android/providers/contacts/testutil/TestUtil.java index 6c8c689a..b9203d7a 100644 --- a/test_common/src/com/android/providers/contacts/testutil/TestUtil.java +++ b/test_common/src/com/android/providers/contacts/testutil/TestUtil.java @@ -59,7 +59,7 @@ public class TestUtil { return uri.buildUpon() .appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName) .appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType) - .appendQueryParameter(RawContacts.DATA_SET, dataSet) + .appendQueryParameter(RawContacts.DATA_SET, dataSet != null ? dataSet : "") .build(); } } diff --git a/tests/Android.bp b/tests/Android.bp index 6fc1d179..beb2d313 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -1,12 +1,6 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "packages_providers_ContactsProvider_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: [ - "packages_providers_ContactsProvider_license", - ], + default_applicable_licenses: ["Android-Apache-2.0"], } android_test { diff --git a/tests/assets/phoneAccountHandleMigration/calllog_oldversion.db b/tests/assets/phoneAccountHandleMigration/calllog_oldversion.db Binary files differnew file mode 100644 index 00000000..e00b9855 --- /dev/null +++ b/tests/assets/phoneAccountHandleMigration/calllog_oldversion.db diff --git a/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db b/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db Binary files differnew file mode 100644 index 00000000..128da646 --- /dev/null +++ b/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db diff --git a/tests/assets/upgradeTest/pre_upgrade1600.sql b/tests/assets/upgradeTest/pre_upgrade1600.sql new file mode 100644 index 00000000..f3aacd04 --- /dev/null +++ b/tests/assets/upgradeTest/pre_upgrade1600.sql @@ -0,0 +1,20 @@ +DELETE FROM accounts; +DELETE FROM settings; +DELETE FROM contacts; +DELETE FROM raw_contacts; +DELETE FROM data; +DELETE FROM data_usage_stat; + +--CREATE TABLE accounts (_id INTEGER PRIMARY KEY AUTOINCREMENT,account_name TEXT, account_type TEXT, data_set TEXT); + +INSERT INTO "accounts" VALUES(1,NULL,NULL,NULL); +INSERT INTO "accounts" VALUES(2,"visible","type1",NULL); +INSERT INTO "accounts" VALUES(3,"visible","type1","ds_not_visible"); +INSERT INTO "accounts" VALUES(4,"not_syncable","type1",NULL); +INSERT INTO "accounts" VALUES(5,"no_settings","type2",NULL); + +--CREATE TABLE settings (account_name STRING NOT NULL,account_type STRING NOT NULL,data_set STRING,ungrouped_visible INTEGER NOT NULL DEFAULT 0,should_sync INTEGER NOT NULL DEFAULT 1); + +INSERT INTO "settings" VALUES ("visible","type1",NULL,1,1) +INSERT INTO "settings" VALUES ("visible","type1","ds_not_visible",0,1) +INSERT INTO "settings" VALUES ("not_syncable","type1",NULL,0,0) diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java index 816d10d7..54984d29 100644 --- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java @@ -56,6 +56,7 @@ import android.provider.ContactsContract.Settings; import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItems; import android.provider.VoicemailContract; +import android.telephony.SubscriptionManager; import android.test.MoreAsserts; import android.test.mock.MockContentResolver; import android.util.Log; @@ -79,6 +80,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + /** * A common superclass for {@link ContactsProvider2}-related tests. */ @@ -110,6 +114,9 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { protected final static String NO_STRING = new String(""); protected final static Account NO_ACCOUNT = new Account("a", "b"); + ContextWithServiceOverrides mTestContext; + @Mock SubscriptionManager mSubscriptionManager; + /** * Use {@link MockClock#install()} to start using it. * It'll be automatically uninstalled by {@link #tearDown()}. @@ -127,9 +134,13 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { @Override protected void setUp() throws Exception { super.setUp(); + MockitoAnnotations.initMocks(this); + + mTestContext = new ContextWithServiceOverrides(getContext()); + mTestContext.injectSystemService(SubscriptionManager.class, mSubscriptionManager); mActor = new ContactsActor( - getContext(), getContextPackageName(), getProviderClass(), getAuthority()); + mTestContext, getContextPackageName(), getProviderClass(), getAuthority()); mResolver = mActor.resolver; if (mActor.provider instanceof SynchronousContactsProvider2) { getContactsProvider().wipeData(); @@ -219,12 +230,12 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { return ContentUris.parseId(mResolver.insert(uri, values)); } - protected void createSettings(Account account, String shouldSync, String ungroupedVisible) { - createSettings(new AccountWithDataSet(account.name, account.type, null), + protected Uri createSettings(Account account, String shouldSync, String ungroupedVisible) { + return createSettings(new AccountWithDataSet(account.name, account.type, null), shouldSync, ungroupedVisible); } - protected void createSettings(AccountWithDataSet account, String shouldSync, + protected Uri createSettings(AccountWithDataSet account, String shouldSync, String ungroupedVisible) { ContentValues values = new ContentValues(); values.put(Settings.ACCOUNT_NAME, account.getAccountName()); @@ -234,7 +245,7 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { } values.put(Settings.SHOULD_SYNC, shouldSync); values.put(Settings.UNGROUPED_VISIBLE, ungroupedVisible); - mResolver.insert(Settings.CONTENT_URI, values); + return mResolver.insert(Settings.CONTENT_URI, values); } protected Uri insertOrganization(long rawContactId, ContentValues values) { diff --git a/tests/src/com/android/providers/contacts/CallLogMigrationTest.java b/tests/src/com/android/providers/contacts/CallLogMigrationTest.java index 767b62f5..d1e80035 100644 --- a/tests/src/com/android/providers/contacts/CallLogMigrationTest.java +++ b/tests/src/com/android/providers/contacts/CallLogMigrationTest.java @@ -15,10 +15,19 @@ */ package com.android.providers.contacts; +import static com.android.providers.contacts.CallLogDatabaseHelper.DATABASE_VERSION; + +import android.content.ContentValues; import android.content.Context; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.CallLog.Calls; +import android.provider.VoicemailContract; import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import java.io.File; import java.io.FileOutputStream; @@ -28,6 +37,12 @@ import java.io.OutputStream; @LargeTest public class CallLogMigrationTest extends FixedAndroidTestCase { + private final static String TAG = CallLogMigrationTest.class.getSimpleName(); + + // Maximum number for database version that need migration + public static final int DATABASE_VERSION_NEED_MIGRATION = 10; + // Component name for call log entries that don't need migration + public static final String NO_MIGRATION_COMPONENT_NAME = "foo/bar"; private void writeAssetFileToDisk(String assetName, File diskPath) throws IOException { final Context context = getTestContext(); @@ -46,6 +61,38 @@ public class CallLogMigrationTest extends FixedAndroidTestCase { } } + /** Insert a call log to db with specified phone account component name */ + private boolean insertCallLog(SQLiteDatabase db, String componentName) { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, componentName); + return db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values) != -1; + } + + /* + * Test onUpgrade() step, check the IS_PHONE_ACCOUNT_MIGRATION_PENDING column is upgraded. + */ + public void testPhoneAccountMigrationMarkingOnUpgrade() throws IOException { + SQLiteDatabase db = new InMemoryCallLogProviderDbHelperV1(mContext, + DATABASE_VERSION).getWritableDatabase(); + CallLogDatabaseHelperTestable testable = new CallLogDatabaseHelperTestable( + getTestContext(), null); + CallLogDatabaseHelper.OpenHelper openHelper = testable.getOpenHelper(); + // Insert 3 entries that 2 of its is_call_log_phone_account_migration_pending should be set + // to 1 + assertTrue(insertCallLog(db, PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME)); + assertTrue(insertCallLog(db, PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME)); + assertTrue(insertCallLog(db, NO_MIGRATION_COMPONENT_NAME)); + + openHelper.onUpgrade(db, DATABASE_VERSION_NEED_MIGRATION, DATABASE_VERSION); + + // Check each entry in the CALLS has a new coloumn of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has a value of either 0 or 1 + assertEquals(2, DatabaseUtils.longForQuery( + db, "select count(*) from " + CallLogDatabaseHelper.Tables.CALLS + + " where " + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 1", null)); + } + public void testMigration() throws IOException { final File sourceDbFile = new File(getTestContext().getCacheDir(), "contacts2src.db"); writeAssetFileToDisk("calllogmigration/contacts2.db", sourceDbFile); @@ -57,7 +104,6 @@ public class CallLogMigrationTest extends FixedAndroidTestCase { // Make sure the source tables exist initially. assertTrue(CallLogDatabaseHelper.tableExists(sourceDb, "calls")); assertTrue(CallLogDatabaseHelper.tableExists(sourceDb, "voicemail_status")); - // Create the calllog DB to perform the migration. final CallLogDatabaseHelperTestable dbh = new CallLogDatabaseHelperTestable(getTestContext(), sourceDb); @@ -76,6 +122,13 @@ public class CallLogMigrationTest extends FixedAndroidTestCase { assertEquals("123456", dbh.getProperty(CallLogDatabaseHelper.DbProperties.CALL_LOG_LAST_SYNCED, "")); + // Test onCreate() step, check each entry with TelephonyComponent in the CALLS has + // a new coloumn of Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING. + assertEquals(3, + DatabaseUtils.longForQuery(db, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 0", null)); + // Also, the source table should have been removed. assertFalse(CallLogDatabaseHelper.tableExists(sourceDb, "calls")); assertFalse(CallLogDatabaseHelper.tableExists(sourceDb, "voicemail_status")); @@ -84,4 +137,78 @@ public class CallLogMigrationTest extends FixedAndroidTestCase { dbh.getProperty(CallLogDatabaseHelper.DbProperties.DATA_MIGRATED, "")); } } + + public static final class InMemoryCallLogProviderDbHelperV1 extends SQLiteOpenHelper { + public InMemoryCallLogProviderDbHelperV1(Context context, int databaseVersion) { + super(context, + null /* "null" DB name to make it an in-memory DB */, + null /* CursorFactory is null by default */, + databaseVersion); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.d(TAG, "IN MEMORY DB CREATED"); + + db.execSQL("CREATE TABLE " + CallLogDatabaseHelper.Tables.CALLS + " (" + + Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Calls.NUMBER + " TEXT," + + Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT ''," + + Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," + + Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," + + Calls.DATE + " INTEGER," + + Calls.DURATION + " INTEGER," + + Calls.DATA_USAGE + " INTEGER," + + Calls.TYPE + " INTEGER," + + Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," + + Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," + + Calls.PHONE_ACCOUNT_ID + " TEXT," + + Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," + + Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," + + Calls.SUB_ID + " INTEGER DEFAULT -1," + + Calls.NEW + " INTEGER," + + Calls.CACHED_NAME + " TEXT," + + Calls.CACHED_NUMBER_TYPE + " INTEGER," + + Calls.CACHED_NUMBER_LABEL + " TEXT," + + Calls.COUNTRY_ISO + " TEXT," + + Calls.VOICEMAIL_URI + " TEXT," + + Calls.IS_READ + " INTEGER," + + Calls.GEOCODED_LOCATION + " TEXT," + + Calls.CACHED_LOOKUP_URI + " TEXT," + + Calls.CACHED_MATCHED_NUMBER + " TEXT," + + Calls.CACHED_NORMALIZED_NUMBER + " TEXT," + + Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," + + Calls.CACHED_PHOTO_URI + " TEXT," + + Calls.CACHED_FORMATTED_NUMBER + " TEXT," + + Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," + + Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," + + Calls.CALL_SCREENING_COMPONENT_NAME + " TEXT," + + Calls.CALL_SCREENING_APP_NAME + " TEXT," + + Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails._DATA + " TEXT," + + VoicemailContract.Voicemails.HAS_CONTENT + " INTEGER," + + VoicemailContract.Voicemails.MIME_TYPE + " TEXT," + + VoicemailContract.Voicemails.SOURCE_DATA + " TEXT," + + VoicemailContract.Voicemails.SOURCE_PACKAGE + " TEXT," + + VoicemailContract.Voicemails.TRANSCRIPTION + " TEXT," + + VoicemailContract.Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.STATE + " INTEGER," + + VoicemailContract.Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," + + VoicemailContract.Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," + + Calls.MISSED_REASON + " INTEGER NOT NULL DEFAULT 0," + + Calls.PRIORITY + " INTEGER NOT NULL DEFAULT 0," + + Calls.SUBJECT + " TEXT," + + Calls.LOCATION + " TEXT," + + Calls.COMPOSER_PHOTO_URI + " TEXT" + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + } } diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java index 885fbe04..a0a8483f 100644 --- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java +++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java @@ -18,6 +18,8 @@ package com.android.providers.contacts; import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED; +import static com.android.providers.contacts.ContactsProvider2.BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES; + import static org.mockito.Mockito.eq; import static org.mockito.Mockito.when; @@ -26,6 +28,7 @@ import android.telecom.CallerInfo; import com.android.providers.contacts.testutil.CommonDatabaseUtils; import com.android.providers.contacts.util.ContactsPermissions; import com.android.providers.contacts.util.FileUtilities; +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -80,7 +83,7 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { Voicemails.DIRTY, Voicemails.DELETED}; /** Total number of columns exposed by call_log provider. */ - private static final int NUM_CALLLOG_FIELDS = 40; + private static final int NUM_CALLLOG_FIELDS = 41; private static final int MIN_MATCH = 7; @@ -102,9 +105,11 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { private static final String TEST_FAIL_DID_NOT_TRHOW_SE = "fail test because Security Exception was not throw"; + private int mOldMinMatch; private CallLogProviderTestable mCallLogProvider; + private BroadcastReceiver mBroadcastReceiver; @Override protected Class<? extends ContentProvider> getProviderClass() { @@ -120,6 +125,7 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { protected void setUp() throws Exception { super.setUp(); mCallLogProvider = addProvider(CallLogProviderTestable.class, CallLog.AUTHORITY); + mBroadcastReceiver = mCallLogProvider.getBroadcastReceiverForTest(); mOldMinMatch = mCallLogProvider.getMinMatchForTest(); mCallLogProvider.setMinMatchForTest(MIN_MATCH); } @@ -133,6 +139,211 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { super.tearDown(); } + private CallLogDatabaseHelper getMockCallLogDatabaseHelper(String databaseNameForTesting) { + CallLogDatabaseHelper callLogDatabaseHelper = new CallLogDatabaseHelper( + mTestContext, databaseNameForTesting); + SQLiteDatabase db = callLogDatabaseHelper.getWritableDatabase(); + // callLogDatabaseHelper.getOpenHelper().onCreate(db); + db.execSQL("DELETE FROM " + CallLogDatabaseHelper.Tables.CALLS); + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, TEST_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID2); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Calls.PHONE_ACCOUNT_ID, "FAKE_ICCID"); + db.insert(CallLogDatabaseHelper.Tables.CALLS, null, values); + } + return callLogDatabaseHelper; + } + + public void testPhoneAccountHandleMigrationSimEvent() throws IOException { + CallLogDatabaseHelper originalCallLogDatabaseHelper + = mCallLogProvider.getCallLogDatabaseHelperForTest(); + + // Mock SubscriptionManager + SubscriptionInfo subscriptionInfo = new SubscriptionInfo( + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1, + 1, "a", "b", 1, 1, "test", 1, null, null, null, null, false, null, null); + when(mSubscriptionManager.getActiveSubscriptionInfo( + eq(TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT))).thenReturn(subscriptionInfo); + + // Mock CallLogDatabaseHelper + CallLogDatabaseHelper callLogDatabaseHelper = getMockCallLogDatabaseHelper( + "testCallLogPhoneAccountHandleMigrationSimEvent.db"); + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils = callLogDatabaseHelper + .getPhoneAccountHandleMigrationUtils(); + + // Test setPhoneAccountMigrationStatusPending as false + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(false); + assertFalse(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + // Test CallLogDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + mCallLogProvider.setCallLogDatabaseHelperForTest(callLogDatabaseHelper); + final SQLiteDatabase sqLiteDatabase = callLogDatabaseHelper.getReadableDatabase(); + + // Check each entry in the Calls table has a new coloumn of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING of 1 + assertEquals(6, DatabaseUtils.longForQuery(sqLiteDatabase, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 1", null)); + + // Prepare PhoneAccountHandle for the new sim event + PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle( + new ComponentName(TELEPHONY_PACKAGE, TELEPHONY_CLASS), + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID); + Intent intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED); + intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + + mBroadcastReceiver.onReceive(mTestContext, intent); + + // Wait for a while until the migration happens + long countMigrated = 0; + + while (countMigrated != 4) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // do nothing + } + countMigrated = DatabaseUtils.longForQuery(sqLiteDatabase, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 0", null); + } + + // Check each entry in the CALLS that three coloumns of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has migrated + assertEquals(4, countMigrated); + // Check each entry in the CALLS that one coloumns of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING that is not expected to be migrated + assertEquals(2, DatabaseUtils.longForQuery(sqLiteDatabase, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 1", null)); + + // Verify the pending status of phone account migration. + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + mCallLogProvider.setCallLogDatabaseHelperForTest(originalCallLogDatabaseHelper); + } + + + public void testPhoneAccountHandleMigrationInitiation() throws Exception { + CallLogDatabaseHelper originalCallLogDatabaseHelper + = mCallLogProvider.getCallLogDatabaseHelperForTest(); + + // Mock SubscriptionManager + SubscriptionInfo subscriptionInfo = new SubscriptionInfo( + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1, + 1, "a", "b", 1, 1, "test", 1, null, null, null, null, false, null, null); + List<SubscriptionInfo> subscriptionInfoList = new ArrayList<>(); + subscriptionInfoList.add(subscriptionInfo); + when(mSubscriptionManager.getAllSubscriptionInfoList()).thenReturn(subscriptionInfoList); + + // Mock CallLogDatabaseHelper + CallLogDatabaseHelper callLogDatabaseHelper = getMockCallLogDatabaseHelper( + "testCallLogPhoneAccountHandleMigrationInitiation.db"); + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils = callLogDatabaseHelper + .getPhoneAccountHandleMigrationUtils(); + + // Test setPhoneAccountMigrationStatusPending as false + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(false); + assertFalse(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + // Test CallLogDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + + mCallLogProvider.setCallLogDatabaseHelperForTest(callLogDatabaseHelper); + final SQLiteDatabase sqLiteDatabase = callLogDatabaseHelper.getReadableDatabase(); + + // Check each entry in the Calls table has a new coloumn of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING as true + assertEquals(6, DatabaseUtils.longForQuery(sqLiteDatabase, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " == 1", null)); + + // Prepare Task for BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES + mCallLogProvider.mReadAccessLatch = new CountDownLatch(1); + mCallLogProvider.performBackgroundTask(mCallLogProvider.BACKGROUND_TASK_INITIALIZE, null); + assertTrue(mCallLogProvider.mReadAccessLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS)); + + // Check each entry in the CALLS with a coloumn of + // Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has migrated + Cursor cursor = sqLiteDatabase.query(CallLogDatabaseHelper.Tables.CALLS, null, + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 0", null, null, null, null); + assertEquals(4, cursor.getCount()); + while (cursor.moveToNext()) { + assertEquals(TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT, cursor.getInt(cursor.getColumnIndex(Calls.PHONE_ACCOUNT_ID))); + } + assertEquals(2, DatabaseUtils.longForQuery(sqLiteDatabase, "select count(*) from " + + CallLogDatabaseHelper.Tables.CALLS + " where " + + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 1", null)); + + // Verify the pending status of phone account migration. + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + mCallLogProvider.setCallLogDatabaseHelperForTest(originalCallLogDatabaseHelper); + } + + public void testPhoneAccountHandleMigrationPendingStatus() { + // Mock CallLogDatabaseHelper + CallLogDatabaseHelper callLogDatabaseHelper = getMockCallLogDatabaseHelper( + "testPhoneAccountHandleMigrationPendingStatus.db"); + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils = callLogDatabaseHelper + .getPhoneAccountHandleMigrationUtils(); + + // Test setPhoneAccountMigrationStatusPending as false + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(false); + assertFalse(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + // Test CallLogDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + } + public void testInsert_RegularCallRecord() { setTimeForTest(1000L); ContentValues values = getDefaultCallValues(); @@ -237,7 +448,7 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { ContactsPermissions.ALLOW_SELF_CALL = true; Uri uri = Calls.addCall(ci, getMockContext(), "1-800-263-7643", Calls.PRESENTATION_ALLOWED, Calls.OUTGOING_TYPE, 0, subscription, 2000, - 40, null, MISSED_REASON_NOT_MISSED); + 40, null, MISSED_REASON_NOT_MISSED, 0); ContactsPermissions.ALLOW_SELF_CALL = false; assertNotNull(uri); assertEquals("0@" + CallLog.AUTHORITY, uri.getAuthority()); @@ -261,6 +472,7 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { // parameters and the compiler needs a hint as to which form is correct. values.put(Calls.DATA_USAGE, (Long) null); values.put(Calls.MISSED_REASON, 0); + values.put(Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 0); assertStoredValues(uri, values); } @@ -526,6 +738,16 @@ public class CallLogProviderTest extends BaseContactsProvider2Test { assertEquals(10, mCallLogProvider.getLastSyncTime(/* forShadow =*/ false)); } + public void testNullSubscriptionInfo() { + PhoneAccountHandle handle = new PhoneAccountHandle(new ComponentName( + TELEPHONY_PACKAGE, TELEPHONY_CLASS), TEST_PHONE_ACCOUNT_HANDLE_SUB_ID); + when(mSubscriptionManager.getActiveSubscriptionInfo(eq( + Integer.parseInt(TEST_PHONE_ACCOUNT_HANDLE_SUB_ID)))).thenReturn(null); + + mCallLogProvider.performBackgroundTask( + BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES, handle); + } + private ContentValues getDefaultValues(int callType) { ContentValues values = new ContentValues(); values.put(Calls.TYPE, callType); diff --git a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java index 047e8ea5..eae49b75 100644 --- a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java +++ b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java @@ -40,7 +40,14 @@ import java.util.ArrayList; @MediumTest public class ContactLookupKeyTest extends BaseContactsProvider2Test { + private static final int LOCAL_ACCOUNT_HASH_CODE = + ContactLookupKey.getAccountHashCode( + AccountWithDataSet.LOCAL.getAccountType(), + AccountWithDataSet.LOCAL.getAccountName()); + public void testLookupKeyUsingDisplayNameAndNoAccount() { + int accountHashCode = LOCAL_ACCOUNT_HASH_CODE; + long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe"); long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "johndoe", null); setAggregationException( @@ -48,8 +55,8 @@ public class ContactLookupKeyTest extends BaseContactsProvider2Test { // Normalized display name String normalizedName = NameNormalizer.normalize("johndoe"); - String expectedLookupKey = "0r" + rawContactId1 + "-" + normalizedName + ".0r" - + rawContactId2 + "-" + normalizedName; + String expectedLookupKey = accountHashCode + "r" + rawContactId1 + "-" + normalizedName + + "." + accountHashCode + "r" + rawContactId2 + "-" + normalizedName; long contactId = queryContactId(rawContactId1); assertStoredValue(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), @@ -83,8 +90,12 @@ public class ContactLookupKeyTest extends BaseContactsProvider2Test { setAggregationException( AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId3); + int accountHashCode = LOCAL_ACCOUNT_HASH_CODE; // Two source ids, of them escaped - String expectedLookupKey = "0i123.0e4..5..6.0ihttp%3A%2F%2Ffoo%3Fbar"; + String expectedLookupKey = accountHashCode + "i123." + + accountHashCode + "e4..5..6." + + accountHashCode + "ihttp%3A%2F%2Ffoo%3Fbar"; + long contactId = queryContactId(rawContactId1); assertStoredValue(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), @@ -139,7 +150,9 @@ public class ContactLookupKeyTest extends BaseContactsProvider2Test { setAggregationException( AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId3); - String lookupKey = "0i1.0i2.0i3"; + int accountHashCode = LOCAL_ACCOUNT_HASH_CODE; + String lookupKey = accountHashCode + "i1." + accountHashCode + "i2." + + accountHashCode + "i3"; long contactId = queryContactId(rawContactId1); assertStoredValue(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), @@ -156,10 +169,10 @@ public class ContactLookupKeyTest extends BaseContactsProvider2Test { long largerContactId = queryContactId(rawContactId1); assertStoredValue( ContentUris.withAppendedId(Contacts.CONTENT_URI, largerContactId), - Contacts.LOOKUP_KEY, "0i1.0i2"); + Contacts.LOOKUP_KEY, accountHashCode + "i1." + accountHashCode + "i2"); assertStoredValue( ContentUris.withAppendedId(Contacts.CONTENT_URI, queryContactId(rawContactId3)), - Contacts.LOOKUP_KEY, "0i3"); + Contacts.LOOKUP_KEY, accountHashCode + "i3"); Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey); assertStoredValue(lookupUri, Contacts._ID, largerContactId); @@ -169,15 +182,17 @@ public class ContactLookupKeyTest extends BaseContactsProvider2Test { long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe"); storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.SOURCE_ID, "1"); + int accountHashCode = LOCAL_ACCOUNT_HASH_CODE; long contactId = queryContactId(rawContactId1); - String lookupUri = "content://com.android.contacts/contacts/lookup/0i1/" + contactId; + String lookupUri = "content://com.android.contacts/contacts/lookup/" + + accountHashCode + "i1/" + contactId; Uri contentUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); assertEquals(lookupUri, Contacts.getLookupUri(mResolver, contentUri).toString()); Uri staleLookupUri = ContentUris.withAppendedId( - Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, "0i1"), + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, accountHashCode + "i1"), contactId+2); assertEquals(lookupUri, Contacts.getLookupUri(mResolver, staleLookupUri).toString()); diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java index 56a4fc4b..e3c606e2 100644 --- a/tests/src/com/android/providers/contacts/ContactsActor.java +++ b/tests/src/com/android/providers/contacts/ContactsActor.java @@ -191,7 +191,7 @@ public class ContactsActor { } @Override - public int getUserHandle() { + public int getProcessUserId() { return myUser; } @@ -404,7 +404,7 @@ public class ContactsActor { @Override public int getUserId() { if (mockUserManager != null) { - return mockUserManager.getUserHandle(); + return mockUserManager.getProcessUserId(); } else { return DEFAULT_USER_ID; } diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java index a9867c53..2e2e9242 100644 --- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java +++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java @@ -18,6 +18,7 @@ package com.android.providers.contacts; import android.content.ContentValues; import android.database.ContentObserver; +import android.accounts.Account; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; @@ -508,4 +509,46 @@ public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test { assertEquals(creationTime, dbHelper2.getDatabaseCreationTime()); } + + public void testGetAndSetDefaultAccount() { + Account account = mDbHelper.getDefaultAccount(); + assertNull(account); + + mDbHelper.setDefaultAccount("a", "b"); + account = mDbHelper.getDefaultAccount(); + assertEquals("a", account.name); + assertEquals("b", account.type); + + mDbHelper.setDefaultAccount("c", "d"); + account = mDbHelper.getDefaultAccount(); + assertEquals("c", account.name); + assertEquals("d", account.type); + + mDbHelper.setDefaultAccount(null, null); + account = mDbHelper.getDefaultAccount(); + assertNull(account); + + // Invalid account (not-null account name and null account type) throws exception. + try { + mDbHelper.setDefaultAccount("name", null); + fail("Setting default account to an invalid account should fail."); + } catch (IllegalArgumentException e) { + // expected. + } + account = mDbHelper.getDefaultAccount(); + assertNull(account); + + // Update default account to an existing account + mDbHelper.setDefaultAccount("a", "b"); + account = mDbHelper.getDefaultAccount(); + assertEquals("a", account.name); + assertEquals("b", account.type); + + try (Cursor cursor = mDbHelper.getReadableDatabase().query(Tables.ACCOUNTS, new String[]{ + ContactsDatabaseHelper.AccountsColumns.ACCOUNT_NAME, + ContactsDatabaseHelper.AccountsColumns.ACCOUNT_TYPE + }, null, null, null, null, null)) { + assertEquals(3, cursor.getCount()); + } + } } diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java index d50a2922..0548f980 100644 --- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java +++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java @@ -33,7 +33,6 @@ import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.PhotoFiles; import android.provider.ContactsContract.PinnedPositions; import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.Settings; import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItemPhotos; import android.provider.ContactsContract.StreamItems; @@ -122,6 +121,8 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade int oldVersion = 1108; oldVersion = upgradeTo1109(oldVersion); + oldVersion = upgradeTo1600(oldVersion); + oldVersion = upgradeTo1601(oldVersion); oldVersion = upgrade(oldVersion, ContactsDatabaseHelper.DATABASE_VERSION); assertEquals(ContactsDatabaseHelper.DATABASE_VERSION, oldVersion); assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ false); @@ -192,6 +193,110 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade return MY_VERSION; } + private int upgradeTo1600(int upgradeFrom) { + final int MY_VERSION = 1600; + + executeSqlFromAssetFile(getTestContext(), mDb, "upgradeTest/pre_upgrade1600.sql"); + + mHelper.onUpgrade(mDb, upgradeFrom, MY_VERSION); + + try (Cursor c = mDb.rawQuery("select " + + "_id, account_name, account_type, data_set, ungrouped_visible, should_sync " + + "from accounts order by _id", null)) { + BaseContactsProvider2Test.assertCursorValuesOrderly(c, + cv(Contacts._ID, 1, + "account_name", null, + "account_type", null, + "data_set", null, + "ungrouped_visible", 1, + "should_sync", 0 + ), + cv(Contacts._ID, 2, + "account_name", "visible", + "account_type", "type1", + "data_set", null, + "ungrouped_visible", 1, + "should_sync", 1 + ), + cv(Contacts._ID, 3, + "account_name", "visible", + "account_type", "type1", + "data_set", "ds_not_visible", + "ungrouped_visible", 0, + "should_sync", 1 + ), + cv(Contacts._ID, 4, + "account_name", "not_syncable", + "account_type", "type1", + "data_set", null, + "ungrouped_visible", 0, + "should_sync", 0 + ), + cv(Contacts._ID, 5, + "account_name", "no_settings", + "account_type", "type2", + "data_set", null, + "ungrouped_visible", 0, + "should_sync", 1 + )); + } + + return MY_VERSION; + } + + private int upgradeTo1601(int upgradeFrom) { + final int MY_VERSION = 1601; + + mHelper.onUpgrade(mDb, upgradeFrom, MY_VERSION); + + try (Cursor c = mDb.rawQuery("select " + + "_id, account_name, account_type, data_set, ungrouped_visible, should_sync, " + + "x_is_default from accounts order by _id", null)) { + BaseContactsProvider2Test.assertCursorValuesOrderly(c, + cv(Contacts._ID, 1, + "account_name", null, + "account_type", null, + "data_set", null, + "ungrouped_visible", 1, + "should_sync", 0, + "x_is_default", 0 + ), + cv(Contacts._ID, 2, + "account_name", "visible", + "account_type", "type1", + "data_set", null, + "ungrouped_visible", 1, + "should_sync", 1, + "x_is_default", 0 + ), + cv(Contacts._ID, 3, + "account_name", "visible", + "account_type", "type1", + "data_set", "ds_not_visible", + "ungrouped_visible", 0, + "should_sync", 1, + "x_is_default", 0 + ), + cv(Contacts._ID, 4, + "account_name", "not_syncable", + "account_type", "type1", + "data_set", null, + "ungrouped_visible", 0, + "should_sync", 0, + "x_is_default", 0 + ), + cv(Contacts._ID, 5, + "account_name", "no_settings", + "account_type", "type2", + "data_set", null, + "ungrouped_visible", 0, + "should_sync", 1, + "x_is_default", 0 + )); + } + return MY_VERSION; + } + private int upgrade(int upgradeFrom, int upgradeTo) { if (upgradeFrom < upgradeTo) { mHelper.onUpgrade(mDb, upgradeFrom, upgradeTo); @@ -222,6 +327,9 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade new TableColumn(AccountsColumns.DATA_SET, TEXT, false, null), new TableColumn(AccountsColumns.SIM_SLOT_INDEX, INTEGER, false, null), new TableColumn(AccountsColumns.SIM_EF_TYPE, INTEGER, false, null), + new TableColumn(AccountsColumns.UNGROUPED_VISIBLE, INTEGER, true, "0"), + new TableColumn(AccountsColumns.SHOULD_SYNC, INTEGER, true, "1"), + new TableColumn(AccountsColumns.IS_DEFAULT, INTEGER, true, "0") }; private static final TableColumn[] CONTACTS_COLUMNS = new TableColumn[] { @@ -364,6 +472,7 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade new TableColumn(Data.SYNC3, TEXT, false, null), new TableColumn(Data.SYNC4, TEXT, false, null), new TableColumn(Data.CARRIER_PRESENCE, INTEGER, true, "0"), + new TableColumn(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, INTEGER, true, "0"), new TableColumn(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, TEXT, false, null), new TableColumn(Data.PREFERRED_PHONE_ACCOUNT_ID, TEXT, false, null), }; @@ -417,14 +526,6 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade new TableColumn(AggregationExceptions.RAW_CONTACT_ID2, INTEGER, false, null), }; - private static final TableColumn[] SETTINGS_COLUMNS = new TableColumn[] { - new TableColumn(Settings.ACCOUNT_NAME, STRING, true, null), - new TableColumn(Settings.ACCOUNT_TYPE, STRING, true, null), - new TableColumn(Settings.DATA_SET, STRING, false, null), - new TableColumn(Settings.UNGROUPED_VISIBLE, INTEGER, true, "0"), - new TableColumn(Settings.SHOULD_SYNC, INTEGER, true, "1"), - }; - private static final TableColumn[] VISIBLE_CONTACTS_COLUMNS = new TableColumn[] { new TableColumn(Contacts._ID, INTEGER, false, null), }; @@ -567,7 +668,6 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade new TableListEntry(Tables.NICKNAME_LOOKUP, NICKNAME_LOOKUP_COLUMNS), new TableListEntry(Tables.GROUPS, GROUPS_COLUMNS), new TableListEntry(Tables.AGGREGATION_EXCEPTIONS, AGGREGATION_EXCEPTIONS_COLUMNS), - new TableListEntry(Tables.SETTINGS, SETTINGS_COLUMNS), new TableListEntry(Tables.VISIBLE_CONTACTS, VISIBLE_CONTACTS_COLUMNS), new TableListEntry(Tables.DEFAULT_DIRECTORY, DEFAULT_DIRECTORY_COLUMNS), new TableListEntry("calls", CALLS_COLUMNS, false), diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseMigrationTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseMigrationTest.java new file mode 100644 index 00000000..6ef14cf1 --- /dev/null +++ b/tests/src/com/android/providers/contacts/ContactsDatabaseMigrationTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 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.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.provider.ContactsContract.Data; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +@LargeTest +public class ContactsDatabaseMigrationTest extends FixedAndroidTestCase { + + public static final int NUM_ENTRIES_CONTACTS_DB_OLD_VERSION = 11; + + private void writeAssetFileToDisk(String assetName, File diskPath) throws IOException { + final Context context = getTestContext(); + final byte[] BUF = new byte[1024 * 32]; + + try (final InputStream input = context.getAssets().open(assetName)) { + try (final OutputStream output = new FileOutputStream(diskPath)) { + for (;;) { + final int len = input.read(BUF); + if (len == -1) { + break; + } + output.write(BUF, 0, len); + } + } + } + } + + /* + * Test onUpgrade() step, check the IS_PHONE_ACCOUNT_MIGRATION_PENDING column is upgraded. + */ + public void testPhoneAccountHandleMigrationMarkingOnUpgrade() throws IOException { + final File sourceDbFile = getTestContext().getDatabasePath("contacts2.db"); + writeAssetFileToDisk( + "phoneAccountHandleMigration/contacts2_oldversion.db", sourceDbFile); + + try (final SQLiteDatabase sourceDb = SQLiteDatabase.openDatabase( + sourceDbFile.getAbsolutePath(), /* cursorFactory=*/ null, + SQLiteDatabase.OPEN_READWRITE)) { + + final ContactsDatabaseHelper contactsDatabaseHelper = new ContactsDatabaseHelper( + getTestContext(), "contacts2.db", true, /* isTestInstance=*/ false); + + final SQLiteDatabase sqLiteDatabase = contactsDatabaseHelper.getReadableDatabase(); + + // Check each entry in the Data has a new coloumn of + // Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has a value of either 0 or 1 + assertEquals(NUM_ENTRIES_CONTACTS_DB_OLD_VERSION /** preconfigured entries */, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " >= 0", null)); + + assertEquals(3 /** preconfigured entries for telephony component*/, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " == 1", null)); + + assertEquals(8 /** preconfigured entries for no telephony component*/, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " == 0", null)); + } + } + + /* + * Test onCreate() step, check the IS_PHONE_ACCOUNT_MIGRATION_PENDING column is created + * in the schema. + */ + public void testPhoneAccountHandleMigrationOnCreate() throws IOException { + final ContactsDatabaseHelper contactsDatabaseHelper = new ContactsDatabaseHelper( + getTestContext(), null, true, /* isTestInstance=*/ false); + + final SQLiteDatabase sqLiteDatabase = contactsDatabaseHelper.getReadableDatabase(); + + // Check there is a a new coloumn of Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING created + // in the schema. + assertEquals(0 /** 0 means no entries but the corresponding schema is created */, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " >= 0", null)); + } +}
\ No newline at end of file diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java index 7efc2f47..09ea19fc 100644 --- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java @@ -19,7 +19,12 @@ package com.android.providers.contacts; import static com.android.providers.contacts.TestUtils.cv; import static com.android.providers.contacts.TestUtils.dumpCursor; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; + import android.accounts.Account; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; @@ -27,8 +32,10 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Entity; import android.content.EntityIterator; +import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; @@ -68,7 +75,10 @@ import android.provider.ContactsContract.StatusUpdates; import android.provider.ContactsContract.StreamItemPhotos; import android.provider.ContactsContract.StreamItems; import android.provider.OpenableColumns; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; +import android.telephony.SubscriptionInfo; import android.test.MoreAsserts; import android.test.suitebuilder.annotation.LargeTest; import android.text.TextUtils; @@ -77,6 +87,7 @@ import android.util.ArraySet; import com.android.internal.util.ArrayUtils; import com.android.providers.contacts.ContactsActor.AlteringUserContext; import com.android.providers.contacts.ContactsActor.MockUserManager; +import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns; @@ -84,6 +95,7 @@ import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties; 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.tests.R; import com.android.providers.contacts.testutil.CommonDatabaseUtils; import com.android.providers.contacts.testutil.ContactUtil; import com.android.providers.contacts.testutil.DataUtil; @@ -91,8 +103,8 @@ import com.android.providers.contacts.testutil.DatabaseAsserts; import com.android.providers.contacts.testutil.DeletedContactUtil; import com.android.providers.contacts.testutil.RawContactUtil; import com.android.providers.contacts.testutil.TestUtil; -import com.android.providers.contacts.tests.R; import com.android.providers.contacts.util.NullContentProvider; +import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils; import com.android.providers.contacts.util.UserUtils; import com.google.android.collect.Lists; @@ -125,29 +137,242 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { private static final int MIN_MATCH = 7; + static final String TELEPHONY_PACKAGE = "com.android.phone"; + static final String TELEPHONY_CLASS + = "com.android.services.telephony.TelephonyConnectionService"; + static final String TEST_PHONE_ACCOUNT_HANDLE_SUB_ID = "666"; + static final int TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT = 666; + static final String TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1 = "T6E6S6T6I6C6C6I6D"; + static final String TEST_PHONE_ACCOUNT_HANDLE_ICC_ID2 = "T5E5S5T5I5C5C5I5D"; + static final String TEST_COMPONENT_NAME = "foo/bar"; + private int mOldMinMatch1; private int mOldMinMatch2; + ContactsDatabaseHelper mMockContactsDatabaseHelper; + private ContactsProvider2 mContactsProvider2; + private ContactsDatabaseHelper mDbHelper; + private BroadcastReceiver mBroadcastReceiver; + @Override protected void setUp() throws Exception { super.setUp(); - final ContactsProvider2 cp = (ContactsProvider2) getProvider(); - final ContactsDatabaseHelper dbHelper = cp.getThreadActiveDatabaseHelperForTest(); + mContactsProvider2 = (ContactsProvider2) getProvider(); + mDbHelper = mContactsProvider2.getThreadActiveDatabaseHelperForTest(); + mBroadcastReceiver = mContactsProvider2.getBroadcastReceiverForTest(); mOldMinMatch1 = PhoneNumberUtils.getMinMatchForTest(); - mOldMinMatch2 = dbHelper.getMinMatchForTest(); + mOldMinMatch2 = mDbHelper.getMinMatchForTest(); PhoneNumberUtils.setMinMatchForTest(MIN_MATCH); - dbHelper.setMinMatchForTest(MIN_MATCH); + mDbHelper.setMinMatchForTest(MIN_MATCH); } @Override protected void tearDown() throws Exception { final ContactsProvider2 cp = (ContactsProvider2) getProvider(); - final ContactsDatabaseHelper dbHelper = cp.getThreadActiveDatabaseHelperForTest(); + //final ContactsDatabaseHelper dbHelper = cp.getThreadActiveDatabaseHelperForTest(); PhoneNumberUtils.setMinMatchForTest(mOldMinMatch1); - dbHelper.setMinMatchForTest(mOldMinMatch2); + mDbHelper.setMinMatchForTest(mOldMinMatch2); super.tearDown(); } + private ContactsDatabaseHelper getMockContactsDatabaseHelper(String databaseNameForTesting) { + ContactsDatabaseHelper contactsDatabaseHelper = new ContactsDatabaseHelper( + mTestContext, databaseNameForTesting, true, /* isTestInstance=*/ false); + SQLiteDatabase db = contactsDatabaseHelper.getWritableDatabase(); + db.execSQL("DELETE FROM " + ContactsDatabaseHelper.Tables.DATA); + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, "FAKE_ICCID"); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + { + final ContentValues values = new ContentValues(); + values.put(ContactsDatabaseHelper.DataColumns.MIMETYPE_ID, 6666); + values.put(Data.RAW_CONTACT_ID, 6666); + values.put(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, TEST_COMPONENT_NAME); + values.put(Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, 1); + values.put(Data.PREFERRED_PHONE_ACCOUNT_ID, "FAKE_ICCID"); + long count = db.insert(ContactsDatabaseHelper.Tables.DATA, null, values); + } + return contactsDatabaseHelper; + } + + public void testPhoneAccountHandleMigrationSimEvent() throws IOException { + ContactsDatabaseHelper originalContactsDatabaseHelper + = mContactsProvider2.getContactsDatabaseHelperForTest(); + + // Mock SubscriptionManager + SubscriptionInfo subscriptionInfo = new SubscriptionInfo( + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1, + 1, "a", "b", 1, 1, "test", 1, null, null, null, null, false, null, null); + when(mSubscriptionManager.getActiveSubscriptionInfo( + eq(TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT))).thenReturn(subscriptionInfo); + + // Mock ContactsDatabaseHelper + ContactsDatabaseHelper contactsDatabaseHelper = getMockContactsDatabaseHelper( + "testContactsPhoneAccountHandleMigrationSimEvent.db"); + + // Test setPhoneAccountMigrationStatusPending as false + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils + = contactsDatabaseHelper.getPhoneAccountHandleMigrationUtils(); + + // Test ContactsDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + + mContactsProvider2.setContactsDatabaseHelperForTest(contactsDatabaseHelper); + final SQLiteDatabase sqLiteDatabase = contactsDatabaseHelper.getReadableDatabase(); + + // Check each entry in the Data has a new coloumn of + // Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has a value of 1 + assertEquals(6 /** pending migration entries in the preconfigured file */, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 1", null)); + + // Prepare PhoneAccountHandle for the new sim event + PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle( + new ComponentName(TELEPHONY_PACKAGE, TELEPHONY_CLASS), + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID); + Intent intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED); + intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + mBroadcastReceiver.onReceive(mTestContext, intent); + + // Check four coloumns of Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING have migrated + assertEquals(4 /** entries in the preconfigured database file */, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 0", null)); + // Check two coloumns + // of Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING have not migrated + assertEquals(2 /** pending migration entries after migration in the preconfigured file */, + DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 1", null)); + + mContactsProvider2.setContactsDatabaseHelperForTest(originalContactsDatabaseHelper); + } + + public void testPhoneAccountHandleMigrationInitiation() throws IOException { + ContactsDatabaseHelper originalContactsDatabaseHelper + = mContactsProvider2.getContactsDatabaseHelperForTest(); + + // Mock SubscriptionManager + SubscriptionInfo subscriptionInfo = new SubscriptionInfo( + TEST_PHONE_ACCOUNT_HANDLE_SUB_ID_INT, TEST_PHONE_ACCOUNT_HANDLE_ICC_ID1, + 1, "a", "b", 1, 1, "test", 1, null, null, null, null, false, null, null); + List<SubscriptionInfo> subscriptionInfoList = new ArrayList<>(); + subscriptionInfoList.add(subscriptionInfo); + when(mSubscriptionManager.getAllSubscriptionInfoList()).thenReturn(subscriptionInfoList); + + // Mock ContactsDatabaseHelper + ContactsDatabaseHelper contactsDatabaseHelper = getMockContactsDatabaseHelper( + "testContactsPhoneAccountHandleMigrationInitiation.db"); + + // Test setPhoneAccountMigrationStatusPending as false + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils + = contactsDatabaseHelper.getPhoneAccountHandleMigrationUtils(); + + // Test ContactsDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + + mContactsProvider2.setContactsDatabaseHelperForTest(contactsDatabaseHelper); + final SQLiteDatabase sqLiteDatabase = contactsDatabaseHelper.getReadableDatabase(); + + // Check each entry in the Data has a new coloumn of + // Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING that has a value of 1 + assertEquals(6, DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 1", null)); + + // Prepare Task for BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES + mContactsProvider2.performBackgroundTask( + mContactsProvider2.BACKGROUND_TASK_MIGRATE_PHONE_ACCOUNT_HANDLES, null); + + // Check four coloumns of Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING have migrated + assertEquals(4, DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 0", null)); + // Check two coloumns of Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING have not migrated + assertEquals(2, DatabaseUtils.longForQuery(sqLiteDatabase, + "select count(*) from " + ContactsDatabaseHelper.Tables.DATA + + " where " + Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING + + " = 1", null)); + + // Verify the pending status of phone account migration. + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + mContactsProvider2.setContactsDatabaseHelperForTest(originalContactsDatabaseHelper); + } + + public void testPhoneAccountHandleMigrationPendingStatus() { + // Mock ContactsDatabaseHelper + ContactsDatabaseHelper contactsDatabaseHelper = getMockContactsDatabaseHelper( + "testPhoneAccountHandleMigrationPendingStatus.db"); + + // Test setPhoneAccountMigrationStatusPending as false + PhoneAccountHandleMigrationUtils phoneAccountHandleMigrationUtils + = contactsDatabaseHelper.getPhoneAccountHandleMigrationUtils(); + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(false); + assertFalse(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + + // Test ContactsDatabaseHelper.isPhoneAccountMigrationPending as true + // and set for testing migration logic + phoneAccountHandleMigrationUtils.setPhoneAccountMigrationStatusPending(true); + assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending()); + } + public void testConvertEnterpriseUriWithEnterpriseDirectoryToLocalUri() { String phoneNumber = "886"; String directory = String.valueOf(Directory.ENTERPRISE_DEFAULT); @@ -4242,6 +4467,136 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { new String[] {"c", "d", "plus"}, Settings.SHOULD_SYNC, "0"); } + public void testSettingsDeletion() { + Account account = new Account("a", "b"); + Uri settingUri = createSettings(account, "0", "1"); + long rawContactId = RawContactUtil.createRawContact(mResolver, account); + + int count = mResolver.delete(settingUri, null, null); + + // Settings cannot be deleted when there are still raw contacts for the account. + assertEquals(0, count); + + assertStoredValue(Settings.CONTENT_URI, + Settings.ACCOUNT_NAME + "= ? AND " + Settings.ACCOUNT_TYPE + "= ?", + new String[] {"a", "b"}, Settings.UNGROUPED_VISIBLE, "1"); + + RawContactUtil.delete(mResolver, rawContactId, true); + + count = mResolver.delete(settingUri, null, null); + + assertEquals(1, count); + assertRowCount(0, Settings.CONTENT_URI, null, null); + } + + public void testSettingsUpdate() { + Account account1 = new Account("a", "b"); + Account account2 = new Account("c", "d"); + Account account3 = new Account("e", "f"); + createSettings(account1, "0", "0"); + createSettings(account2, "0", "0"); + createSettings(account3, "0", "0"); + + ContentValues values = new ContentValues(); + values.put(Settings.UNGROUPED_VISIBLE, 1); + int count = mResolver.update(Settings.CONTENT_URI, values, null, null); + + assertEquals(3, count); + assertStoredValues(Settings.CONTENT_URI, + cv(Settings.UNGROUPED_VISIBLE, 1), + cv(Settings.UNGROUPED_VISIBLE, 1), + cv(Settings.UNGROUPED_VISIBLE, 1)); + + values.put(Settings.SHOULD_SYNC, 1); + count = mResolver.update(Settings.CONTENT_URI, values, + Settings.ACCOUNT_NAME + "=?", new String[] {"a"}); + + assertEquals(1, count); + assertStoredValues(Settings.CONTENT_URI, + cv(Settings.ACCOUNT_NAME, "a", + Settings.SHOULD_SYNC, 1), + cv(Settings.ACCOUNT_NAME, "c", + Settings.SHOULD_SYNC, 0), + cv(Settings.ACCOUNT_NAME, "e", + Settings.SHOULD_SYNC, 0)); + + values.clear(); + // Settings are stored in the accounts table but updates shouldn't be allowed to modify + // the other non-Settings columns. + values.put(Settings.ACCOUNT_NAME, "x"); + values.put(Settings.ACCOUNT_TYPE, "y"); + values.put(Settings.DATA_SET, "z"); + mResolver.update(Settings.CONTENT_URI, values, null, null); + + values.put(AccountsColumns.SIM_EF_TYPE, 1); + values.put(AccountsColumns.SIM_SLOT_INDEX, 1); + try { + mResolver.update(Settings.CONTENT_URI, values, null, null); + } catch (Exception e) { + // ignored. We just care that the update didn't change the data + } + + assertStoredValuesDb("SELECT * FROM " + Tables.ACCOUNTS, null, + cv( + Settings.ACCOUNT_NAME, "a", + Settings.ACCOUNT_TYPE, "b", + Settings.DATA_SET, null, + AccountsColumns.SIM_SLOT_INDEX, null, + AccountsColumns.SIM_EF_TYPE, null + ), + cv( + Settings.ACCOUNT_NAME, "c", + Settings.ACCOUNT_TYPE, "d", + Settings.DATA_SET, null, + AccountsColumns.SIM_SLOT_INDEX, null, + AccountsColumns.SIM_EF_TYPE, null + ), + cv( + Settings.ACCOUNT_NAME, "e", + Settings.ACCOUNT_TYPE, "f", + Settings.DATA_SET, null, + AccountsColumns.SIM_SLOT_INDEX, null, + AccountsColumns.SIM_EF_TYPE, null + )); + } + + public void testSettingsLocalAccount() { + AccountWithDataSet localAccount = AccountWithDataSet.LOCAL; + + // It's not possible to insert the local account directly into settings but it will be + // created automatically when a raw contact is created for it. + RawContactUtil.createRawContactWithAccountDataSet( + mResolver, localAccount.getAccountName(), + localAccount.getAccountType(), localAccount.getDataSet()); + + ContentValues values = new ContentValues(); + values.put(Settings.ACCOUNT_NAME, localAccount.getAccountName()); + values.put(Settings.ACCOUNT_TYPE, localAccount.getAccountType()); + values.put(Settings.DATA_SET, localAccount.getDataSet()); + ContentValues expectedValues = new ContentValues(values); + // The defaults for the local account are opposite of other accounts. + expectedValues.put(Settings.UNGROUPED_VISIBLE, "1"); + expectedValues.put(Settings.SHOULD_SYNC, "0"); + + assertStoredValues(Settings.CONTENT_URI, expectedValues); + + values.put(Settings.SHOULD_SYNC, 1); + values.put(Settings.UNGROUPED_VISIBLE, 0); + mResolver.update(Settings.CONTENT_URI, values, null, null); + + expectedValues.put(Settings.UNGROUPED_VISIBLE, "0"); + expectedValues.put(Settings.SHOULD_SYNC, "1"); + assertStoredValues(Settings.CONTENT_URI, expectedValues); + + // Empty strings should also be the local account. + values.put(Settings.ACCOUNT_NAME, ""); + values.put(Settings.ACCOUNT_TYPE, ""); + values.put(Settings.DATA_SET, ""); + mResolver.insert(Settings.CONTENT_URI, values); + + assertRowCount(1, Settings.CONTENT_URI, null, null); + } + public void testDisplayNameParsingWhenPartsUnspecified() { long rawContactId = RawContactUtil.createRawContact(mResolver); ContentValues values = new ContentValues(); @@ -6317,8 +6672,11 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { mActor.setAccounts(new Account[]{mAccount, mAccountTwo}); cp.onAccountsUpdated(new Account[]{mAccount, mAccountTwo}); assertEquals(1, getCount(RawContacts.CONTENT_URI, null, null)); - assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, null); - assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, null); + assertStoredValue( + rawContact3, RawContacts.ACCOUNT_NAME, + AccountWithDataSet.LOCAL.getAccountName()); + assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, + AccountWithDataSet.LOCAL.getAccountType()); long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount); insertEmail(rawContactId1, "account1@email.com"); @@ -7329,6 +7687,55 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { } + public void testCleanupDanglingContacts_noDanglingContacts() throws Exception { + SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider; + RawContactUtil.createRawContactWithName(mResolver, "A", "B"); + RawContactUtil.createRawContactWithName(mResolver, "C", "D"); + + provider.cleanupDanglingContacts(); + + Cursor contactCursor = mResolver.query(Contacts.CONTENT_URI, null, null, null, null); + Cursor rawContactCursor = mResolver.query(RawContacts.CONTENT_URI, null, null, null, null); + + // No contacts should be deleted + assertEquals(2, contactCursor.getCount()); + assertEquals(2, rawContactCursor.getCount()); + } + + public void testCleanupDanglingContacts_singleDanglingContacts() throws Exception { + SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider; + long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "A", "B"); + + // Change the contact_id to create dangling contact. + SQLiteDatabase db = provider.getDatabaseHelper().getWritableDatabase(); + db.execSQL("UPDATE raw_contacts SET contact_id = 99999 WHERE _id = " + rawContactId + ";"); + + provider.cleanupDanglingContacts(); + + // Dangling contact should be deleted from contacts table. + assertEquals(0, mResolver.query(Contacts.CONTENT_URI, null, null, null, null).getCount()); + } + + public void testCleanupDanglingContacts_multipleDanglingContacts() throws Exception { + SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider; + long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "A", "B"); + long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "C", "D"); + RawContactUtil.createRawContactWithName(mResolver, "E", "F"); + + final ContactsDatabaseHelper helper = provider.getDatabaseHelper(); + SQLiteDatabase db = helper.getWritableDatabase(); + + // Change contact_id of RawContact1 and RawContact2 to create dangling contacts. + db.execSQL("UPDATE raw_contacts SET contact_id = 99998 WHERE _id = " + rawContactId1 + ";"); + db.execSQL("UPDATE raw_contacts SET contact_id = 99999 WHERE _id = " + rawContactId2 + ";"); + + provider.cleanupDanglingContacts(); + + // Should only be one contact left in the contacts table. + // RawContact1 and RawContact2 should be deleted from the contacts table. + assertEquals(1, mResolver.query(Contacts.CONTENT_URI, null, null, null, null).getCount()); + } + public void testOverwritePhotoWithThumbnail() throws IOException { long rawContactId = RawContactUtil.createRawContactWithName(mResolver); long contactId = queryContactId(rawContactId); @@ -8944,6 +9351,79 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { ); } + public void testDefaultAccountSet_throwException() { + mActor.setAccounts(new Account[]{mAccount}); + try { + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, null); + fail(); + } catch (SecurityException expected) { + } + + mActor.addPermissions("android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"); + try { + Bundle bundle = new Bundle(); + bundle.putString(Settings.ACCOUNT_NAME, "account1"); // no account type specified + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, bundle); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Bundle bundle = new Bundle(); + bundle.putString(Settings.ACCOUNT_NAME, "account1"); + bundle.putString(Settings.ACCOUNT_TYPE, "account type1"); + bundle.putString(Settings.DATA_SET, "c"); // data set should not be set. + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, bundle); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Bundle bundle = new Bundle(); + bundle.putString(Settings.ACCOUNT_NAME, "account2"); // invalid account + bundle.putString(Settings.ACCOUNT_TYPE, "account type2"); + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, bundle); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testDefaultAccountSetAndQuery() { + Bundle response = mResolver.call(ContactsContract.AUTHORITY_URI, + Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null, null); + Account account = response.getParcelable(Settings.KEY_DEFAULT_ACCOUNT); + assertNull(account); + + mActor.addPermissions("android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"); + mActor.setAccounts(new Account[]{mAccount}); + // Set ("account1", "account type1") account as the default account. + Bundle bundle = new Bundle(); + bundle.putString(Settings.ACCOUNT_NAME, "account1"); + bundle.putString(Settings.ACCOUNT_TYPE, "account type1"); + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, bundle); + + response = mResolver.call(ContactsContract.AUTHORITY_URI, + Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null, null); + account = response.getParcelable(Settings.KEY_DEFAULT_ACCOUNT); + assertEquals("account1", account.name); + assertEquals("account type1", account.type); + + // Set NULL account as default account. + bundle = new Bundle(); + mResolver.call(ContactsContract.AUTHORITY_URI, Settings.SET_DEFAULT_ACCOUNT_METHOD, + null, bundle); + + response = mResolver.call(ContactsContract.AUTHORITY_URI, + Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null, null); + account = response.getParcelable(Settings.KEY_DEFAULT_ACCOUNT); + assertNull(account); + } + public void testPinnedPositionsDemoteIllegalArguments() { try { mResolver.call(ContactsContract.AUTHORITY_URI, PinnedPositions.UNDEMOTE_METHOD, diff --git a/tests/src/com/android/providers/contacts/ContextWithServiceOverrides.java b/tests/src/com/android/providers/contacts/ContextWithServiceOverrides.java new file mode 100644 index 00000000..c230cea4 --- /dev/null +++ b/tests/src/com/android/providers/contacts/ContextWithServiceOverrides.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.content.ContextWrapper; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +public class ContextWithServiceOverrides extends ContextWrapper { + private static final String TAG = "ContextWithOverrides"; + + private Map<String, Object> mInjectedSystemServices = new HashMap<>(); + + public ContextWithServiceOverrides(Context base) { + super(base); + } + + public <S> void injectSystemService(Class<S> cls, S service) { + final String name = getSystemServiceName(cls); + mInjectedSystemServices.put(name, service); + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public Object getSystemService(String name) { + if (mInjectedSystemServices.containsKey(name)) { + return mInjectedSystemServices.get(name); + } + return super.getSystemService(name); + } +}
\ No newline at end of file diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java index 56f08835..37e3184c 100644 --- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java +++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java @@ -1396,7 +1396,7 @@ public class ContactAggregatorTest extends BaseContactsProvider2Test { // Action: make raw contact 2's name super primary storeValue(nameUri2, Data.IS_SUPER_PRIMARY, 1); - // Sanity check. + // Initial check. assertStoredValue(nameUri1, Data.IS_SUPER_PRIMARY, 0); assertStoredValue(nameUri2, Data.IS_SUPER_PRIMARY, 1); @@ -1750,7 +1750,7 @@ public class ContactAggregatorTest extends BaseContactsProvider2Test { final Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1", null, null, /* isSuperPrimary = */ true); - // Sanity check. + // Initial check. assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1); // Action: aggregate diff --git a/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java b/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java index 6eb8b559..5a40bec7 100644 --- a/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java +++ b/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java @@ -54,4 +54,19 @@ public class MoreDatabaseUtilTest extends TestCase { assertEquals("testtable_testfield_index", MoreDatabaseUtils.buildIndexName("testtable", "testfield")); } + + public void testSqlEscapeNullableString() { + assertEquals("NULL", MoreDatabaseUtils.sqlEscapeNullableString(null)); + assertEquals("'foo'", MoreDatabaseUtils.sqlEscapeNullableString("foo")); + } + + public void testAppendEscapedSQLStringOrLiteralNull() { + StringBuilder sb1 = new StringBuilder(); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull(sb1, null); + assertEquals("NULL", sb1.toString()); + + StringBuilder sb2 = new StringBuilder(); + MoreDatabaseUtils.appendEscapedSQLStringOrLiteralNull(sb2, "foo"); + assertEquals("'foo'", sb2.toString()); + } } diff --git a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java index c0684594..2e5241fb 100644 --- a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java +++ b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java @@ -16,7 +16,6 @@ package com.android.providers.contacts.enterprise; import android.app.admin.DevicePolicyManager; -import android.content.ContentResolver; import android.content.Context; import android.content.pm.UserInfo; import android.net.Uri; @@ -265,7 +264,7 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { List<UserInfo> userInfos = MANAGED_USERINFO_LIST; UserManager mockUm = mock(UserManager.class); - when(mockUm.getUserHandle()).thenReturn(CURRENT_USER_ID); + when(mockUm.getProcessUserId()).thenReturn(CURRENT_USER_ID); when(mockUm.getUsers()).thenReturn(userInfos); when(mockUm.getProfiles(Matchers.anyInt())).thenReturn(userInfos); when(mockUm.getProfileParent(WORK_USER_ID)).thenReturn(CURRENT_USER_INFO); diff --git a/tests2/Android.bp b/tests2/Android.bp deleted file mode 100644 index 3dcdebbc..00000000 --- a/tests2/Android.bp +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (C) 2016 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 { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "packages_providers_ContactsProvider_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: [ - "packages_providers_ContactsProvider_license", - ], -} - -android_test { - name: "ContactsProviderTests2", - static_libs: [ - "ContactsProviderTestUtils", - "androidx.test.rules", - "mockito-target-minus-junit4", - ], - libs: [ - "android.test.runner", - "android.test.base", - ], - srcs: ["src/**/*.java"], - platform_apis: true, - test_suites: ["device-tests"], - instrumentation_for: "ContactsProvider", - certificate: "shared", - optimize: { - enabled: false, - }, -} diff --git a/tests2/AndroidManifest.xml b/tests2/AndroidManifest.xml deleted file mode 100644 index fc00251c..00000000 --- a/tests2/AndroidManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 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. ---> - -<!-- - Another unit tests against CP2 that runs in a separate process. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.providers.contacts.tests2" > - - <uses-permission android:name="android.permission.GET_ACCOUNTS" /> - <uses-permission android:name="android.permission.READ_CONTACTS" /> - <uses-permission android:name="android.permission.WRITE_CONTACTS" /> - - <application> - <uses-library android:name="android.test.runner" /> - </application> - - <instrumentation - android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.providers.contacts.tests2" /> -</manifest> diff --git a/tests2/AndroidTest.xml b/tests2/AndroidTest.xml deleted file mode 100644 index 957350b0..00000000 --- a/tests2/AndroidTest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> -<configuration description="Runs Contacts Provider Tests."> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> - <option name="test-file-name" value="ContactsProviderTests2.apk" /> - </target_preparer> - - <option name="test-suite-tag" value="apct" /> - <option name="test-tag" value="ContactsProviderTests2" /> - <test class="com.android.tradefed.testtype.AndroidJUnitTest" > - <option name="package" value="com.android.providers.contacts.tests2" /> - <option name="hidden-api-checks" value="false"/> - </test> -</configuration> diff --git a/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java b/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java deleted file mode 100644 index 30fd3be4..00000000 --- a/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java +++ /dev/null @@ -1,730 +0,0 @@ -/* - * Copyright (C) 2016 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.tests2; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.CancellationSignal; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.SyncState; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.LargeTest; -import android.util.Log; - -import junit.framework.AssertionFailedError; - -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; - -/* - * TODO The following operations would fail, not because they're not supported, but because of - * missing parameters. Fix them. -insert for 'content://com.android.contacts/contacts' failed: Aggregate contacts are created automatically -insert for 'content://com.android.contacts/raw_contacts/1/data' failed: mimetype is required -update for 'content://com.android.contacts/raw_contacts/1/stream_items/1' failed: Empty values -insert for 'content://com.android.contacts/data' failed: raw_contact_id is required -insert for 'content://com.android.contacts/settings' failed: Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE; URI: content://com.android.contacts/settings?account_type=1, calling user: com.android.providers.contacts.tests2, calling package:com.android.providers.contacts.tests2 -insert for 'content://com.android.contacts/status_updates' failed: PROTOCOL and IM_HANDLE are required -insert for 'content://com.android.contacts/profile' failed: The profile contact is created automatically -insert for 'content://com.android.contacts/profile/data' failed: raw_contact_id is required -insert for 'content://com.android.contacts/profile/raw_contacts/1/data' failed: mimetype is required -insert for 'content://com.android.contacts/profile/status_updates' failed: PROTOCOL and IM_HANDLE are required - - -openInputStream for 'content://com.android.contacts/contacts/as_multi_vcard/XXX' failed: Caught Exception: Invalid lookup id: XXX -openInputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0 -openOutputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0 -*/ - -/** - * TODO Add test for delete/update/insert too. - * TODO Copy it to CTS - */ -@LargeTest -public class AllUriTest extends AndroidTestCase { - private static final String TAG = "AllUrlTest"; - - // "-" : Query not supported. - // "!" : Can't query because it requires the cross-user permission. - // The following markers are planned, but not implemented and the definition below is not all - // correct yet. - // "d" : supports delete. - // "u" : supports update. - // "i" : supports insert. - // "r" : supports read. - // "w" : supports write. - // "s" : has x_times_contacted and x_last_time_contacted. - // "t" : has x_times_used and x_last_time_used. - private static final String[][] URIs = { - {"content://com.android.contacts/contacts", "sud"}, - {"content://com.android.contacts/contacts/1", "sud"}, - {"content://com.android.contacts/contacts/1/data", "t"}, - {"content://com.android.contacts/contacts/1/entities", "t"}, - {"content://com.android.contacts/contacts/1/suggestions"}, - {"content://com.android.contacts/contacts/1/suggestions/XXX"}, - {"content://com.android.contacts/contacts/1/photo", "r"}, - {"content://com.android.contacts/contacts/1/display_photo", "-r"}, - {"content://com.android.contacts/contacts_corp/1/photo", "-r"}, - {"content://com.android.contacts/contacts_corp/1/display_photo", "-r"}, - - {"content://com.android.contacts/contacts/filter", "s"}, - {"content://com.android.contacts/contacts/filter/XXX", "s"}, - - {"content://com.android.contacts/contacts/lookup/nlookup", "sud"}, - {"content://com.android.contacts/contacts/lookup/nlookup/data", "t"}, - {"content://com.android.contacts/contacts/lookup/nlookup/photo", "tr"}, - - {"content://com.android.contacts/contacts/lookup/nlookup/1", "sud"}, - {"content://com.android.contacts/contacts/lookup/nlookup/1/data"}, - {"content://com.android.contacts/contacts/lookup/nlookup/1/photo", "r"}, - {"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-r"}, - {"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-r"}, - {"content://com.android.contacts/contacts/lookup/nlookup/entities"}, - {"content://com.android.contacts/contacts/lookup/nlookup/1/entities"}, - - {"content://com.android.contacts/contacts/as_vcard/nlookup", "r"}, - {"content://com.android.contacts/contacts/as_multi_vcard/XXX"}, - - {"content://com.android.contacts/contacts/strequent/", "s"}, - {"content://com.android.contacts/contacts/strequent/filter/XXX", "s"}, - - {"content://com.android.contacts/contacts/group/XXX"}, - - {"content://com.android.contacts/contacts/frequent", "s"}, - {"content://com.android.contacts/contacts/delete_usage", "-d"}, - {"content://com.android.contacts/contacts/filter_enterprise?directory=0", "s"}, - {"content://com.android.contacts/contacts/filter_enterprise/XXX?directory=0", "s"}, - - {"content://com.android.contacts/raw_contacts", "siud"}, - {"content://com.android.contacts/raw_contacts/1", "sud"}, - {"content://com.android.contacts/raw_contacts/1/data", "tu"}, - {"content://com.android.contacts/raw_contacts/1/display_photo", "-rw"}, - {"content://com.android.contacts/raw_contacts/1/entity"}, - - {"content://com.android.contacts/raw_contact_entities"}, - {"content://com.android.contacts/raw_contact_entities_corp", "!"}, - - {"content://com.android.contacts/data", "tud"}, - {"content://com.android.contacts/data/1", "tudr"}, - {"content://com.android.contacts/data/phones", "t"}, - {"content://com.android.contacts/data_enterprise/phones", "!"}, - {"content://com.android.contacts/data/phones/1", "tud"}, - {"content://com.android.contacts/data/phones/filter", "t"}, - {"content://com.android.contacts/data/phones/filter/XXX", "t"}, - - {"content://com.android.contacts/data/phones/filter_enterprise?directory=0", "t"}, - {"content://com.android.contacts/data/phones/filter_enterprise/XXX?directory=0", "t"}, - - {"content://com.android.contacts/data/emails", "t"}, - {"content://com.android.contacts/data/emails/1", "tud"}, - {"content://com.android.contacts/data/emails/lookup", "t"}, - {"content://com.android.contacts/data/emails/lookup/XXX", "t"}, - {"content://com.android.contacts/data/emails/filter", "t"}, - {"content://com.android.contacts/data/emails/filter/XXX", "t"}, - {"content://com.android.contacts/data/emails/filter_enterprise?directory=0", "t"}, - {"content://com.android.contacts/data/emails/filter_enterprise/XXX?directory=0", "t"}, - {"content://com.android.contacts/data/emails/lookup_enterprise", "t"}, - {"content://com.android.contacts/data/emails/lookup_enterprise/XXX", "t"}, - {"content://com.android.contacts/data/postals", "t"}, - {"content://com.android.contacts/data/postals/1", "tud"}, - {"content://com.android.contacts/data/usagefeedback/1,2,3", "-u"}, - {"content://com.android.contacts/data/callables/", "t"}, - {"content://com.android.contacts/data/callables/1", "tud"}, - {"content://com.android.contacts/data/callables/filter", "t"}, - {"content://com.android.contacts/data/callables/filter/XXX", "t"}, - {"content://com.android.contacts/data/callables/filter_enterprise?directory=0", "t"}, - {"content://com.android.contacts/data/callables/filter_enterprise/XXX?directory=0", - "t"}, - {"content://com.android.contacts/data/contactables/", "t"}, - {"content://com.android.contacts/data/contactables/filter", "t"}, - {"content://com.android.contacts/data/contactables/filter/XXX", "t"}, - - {"content://com.android.contacts/groups", "iud"}, - {"content://com.android.contacts/groups/1", "ud"}, - {"content://com.android.contacts/groups_summary"}, - {"content://com.android.contacts/syncstate", "iud"}, - {"content://com.android.contacts/syncstate/1", "-ud"}, - {"content://com.android.contacts/profile/syncstate", "iud"}, - {"content://com.android.contacts/phone_lookup/XXX"}, - {"content://com.android.contacts/phone_lookup_enterprise/XXX"}, - {"content://com.android.contacts/aggregation_exceptions", "u"}, - {"content://com.android.contacts/settings", "ud"}, - {"content://com.android.contacts/status_updates", "ud"}, - {"content://com.android.contacts/status_updates/1"}, - {"content://com.android.contacts/search_suggest_query"}, - {"content://com.android.contacts/search_suggest_query/XXX"}, - {"content://com.android.contacts/search_suggest_shortcut/XXX"}, - {"content://com.android.contacts/provider_status"}, - {"content://com.android.contacts/directories", "u"}, - {"content://com.android.contacts/directories/1"}, - {"content://com.android.contacts/directories_enterprise"}, - {"content://com.android.contacts/directories_enterprise/1"}, - {"content://com.android.contacts/complete_name"}, - {"content://com.android.contacts/profile", "su"}, - {"content://com.android.contacts/profile/entities", "s"}, - {"content://com.android.contacts/profile/data", "tud"}, - {"content://com.android.contacts/profile/data/1", "td"}, - {"content://com.android.contacts/profile/photo", "t"}, - {"content://com.android.contacts/profile/display_photo", "-r"}, - {"content://com.android.contacts/profile/as_vcard", "r"}, - {"content://com.android.contacts/profile/raw_contacts", "siud"}, - - // Note this should have supported update... Too late to add. - {"content://com.android.contacts/profile/raw_contacts/1", "sd"}, - {"content://com.android.contacts/profile/raw_contacts/1/data", "tu"}, - {"content://com.android.contacts/profile/raw_contacts/1/entity"}, - {"content://com.android.contacts/profile/status_updates", "ud"}, - {"content://com.android.contacts/profile/raw_contact_entities"}, - {"content://com.android.contacts/display_photo/1", "-r"}, - {"content://com.android.contacts/photo_dimensions"}, - {"content://com.android.contacts/deleted_contacts"}, - {"content://com.android.contacts/deleted_contacts/1"}, - {"content://com.android.contacts/directory_file_enterprise/XXX?directory=0", "-"}, - }; - - private static final String[] ARG1 = {"-1"}; - - private ContentResolver mResolver; - - private ArrayList<String> mFailures; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - mFailures = new ArrayList<>(); - mResolver = getContext().getContentResolver(); - } - - @Override - protected void tearDown() throws Exception { - if (mFailures != null) { - fail("mFailures is not null. Did you forget to call failIfFailed()?"); - } - - super.tearDown(); - } - - private void addFailure(String message, Throwable th) { - Log.e(TAG, "Failed: " + message, th); - - final int MAX = 100; - if (mFailures.size() == MAX) { - mFailures.add("Too many failures."); - } else if (mFailures.size() > MAX) { - // Too many failures already... - } else { - mFailures.add(message); - } - } - - private void failIfFailed() { - if (mFailures == null) { - fail("mFailures is null. Maybe called failIfFailed() twice?"); - } - if (mFailures.size() > 0) { - StringBuilder message = new StringBuilder(); - - if (mFailures.size() > 0) { - Log.e(TAG, "Something went wrong:"); - for (String s : mFailures) { - Log.e(TAG, s); - if (message.length() > 0) { - message.append("\n"); - } - message.append(s); - } - } - mFailures = null; - fail("Following test(s) failed:\n" + message); - } - mFailures = null; - } - - private static Uri getUri(String[] path) { - return Uri.parse(path[0]); - } - - private static boolean supportsQuery(String[] path) { - if (path.length == 1) { - return true; // supports query by default. - } - return !(path[1].contains("-") || path[1].contains("!")); - } - - private static boolean supportsInsert(String[] path) { - return (path.length) >= 2 && path[1].contains("i"); - } - - private static boolean supportsUpdate(String[] path) { - return (path.length) >= 2 && path[1].contains("u"); - } - - private static boolean supportsDelete(String[] path) { - return (path.length) >= 2 && path[1].contains("d"); - } - - private static boolean supportsRead(String[] path) { - return (path.length) >= 2 && path[1].contains("r"); - } - - private static boolean supportsWrite(String[] path) { - return (path.length) >= 2 && path[1].contains("w"); - } - - private String[] getColumns(Uri uri) { - try (Cursor c = mResolver.query(uri, - null, // projection - "1=2", // selection - null, // selection args - null // sort order - )) { - return c.getColumnNames(); - } - } - - private void checkQueryExecutable(Uri uri, - String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - try { - try (Cursor c = mResolver.query(uri, projection, selection, - selectionArgs, sortOrder)) { - c.moveToFirst(); - } - } catch (Throwable th) { - addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - try { - // With CancellationSignal. - try (Cursor c = mResolver.query(uri, projection, selection, - selectionArgs, sortOrder, new CancellationSignal())) { - c.moveToFirst(); - } - } catch (Throwable th) { - addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - try { - // With limit. - try (Cursor c = mResolver.query( - uri.buildUpon().appendQueryParameter( - ContactsContract.LIMIT_PARAM_KEY, "0").build(), - projection, selection, selectionArgs, sortOrder)) { - c.moveToFirst(); - } - } catch (Throwable th) { - addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - - try { - // With account. - try (Cursor c = mResolver.query( - uri.buildUpon() - .appendQueryParameter(RawContacts.ACCOUNT_NAME, "a") - .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "b") - .appendQueryParameter(RawContacts.DATA_SET, "c") - .build(), - projection, selection, selectionArgs, sortOrder)) { - c.moveToFirst(); - } - } catch (Throwable th) { - addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - } - - private void checkQueryNotExecutable(Uri uri, - String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - try { - try (Cursor c = mResolver.query(uri, projection, selection, - selectionArgs, sortOrder)) { - c.moveToFirst(); - } - } catch (Throwable th) { - // pass. - return; - } - addFailure("Query on " + uri + " expected to fail, but succeeded.", null); - } - - /** - * Make sure all URLs are accessible with all arguments = null. - */ - public void testSelect() { - for (String[] path : URIs) { - if (!supportsQuery(path)) continue; - try { - final Uri uri = getUri(path); - - checkQueryExecutable(uri, // uri - null, // projection - null, // selection - null, // selection args - null // sort order - ); - } catch (Throwable th) { - addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th); - } - } - failIfFailed(); - } - - public void testNoHiddenColumns() { - for (String[] path : URIs) { - if (!supportsQuery(path)) continue; - try { - final Uri uri = getUri(path); - - for (String column : getColumns(uri)) { - if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) { - addFailure("Uri " + uri + " returned hidden column " + column, null); - } - } - } catch (Throwable th) { - addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th); - } - } - failIfFailed(); - } - -// Temporarily disabled due to taking too much time. -// /** -// * Make sure all URLs are accessible with a projection. -// */ -// public void testSelectWithProjection() { -// for (String[] path : URIs) { -// if (!supportsQuery(path)) continue; -// final Uri uri = getUri(path); -// -// for (String column : getColumns(uri)) { -// // Some columns are not selectable alone due to bugs, and we don't want to fix them -// // in order to avoid expanding the differences between versions, so here're some -// // hacks to make it work... -// -// String[] projection = {column}; -// -// final String u = path[0]; -// if ((u.startsWith("content://com.android.contacts/status_updates") -// || u.startsWith("content://com.android.contacts/profile/status_updates")) -// && ("im_handle".equals(column) -// || "im_account".equals(column) -// || "protocol".equals(column) -// || "custom_protocol".equals(column) -// || "presence_raw_contact_id".equals(column) -// )) { -// // These columns only show up when the projection contains certain columns. -// -// projection = new String[]{"mode", column}; -// } else if ((u.startsWith("content://com.android.contacts/search_suggest_query") -// || u.startsWith("content://contacts/search_suggest_query")) -// && "suggest_intent_action".equals(column)) { -// // Can't be included in the projection due to a bug in GlobalSearchSupport. -// continue; -// } else if (RawContacts.BACKUP_ID.equals(column)) { -// // Some URIs don't support a projection with BAKCUP_ID only. -// projection = new String[]{RawContacts.BACKUP_ID, RawContacts.SOURCE_ID}; -// } -// -// checkQueryExecutable(uri, -// projection, // projection -// null, // selection -// null, // selection args -// null // sort order -// ); -// } -// } -// failIfFailed(); -// } - - /** - * Make sure all URLs are accessible with a selection. - */ - public void testSelectWithSelection() { - for (String[] path : URIs) { - if (!supportsQuery(path)) continue; - final Uri uri = getUri(path); - - checkQueryExecutable(uri, - null, // projection - "1=?", // selection - ARG1, // , // selection args - null // sort order - ); - } - failIfFailed(); - } - -// /** -// * Make sure all URLs are accessible with a selection. -// */ -// public void testSelectWithSelectionUsingColumns() { -// for (String[] path : URIs) { -// if (!supportsQuery(path)) continue; -// final Uri uri = getUri(path); -// -// for (String column : getColumns(uri)) { -// checkQueryExecutable(uri, -// null, // projection -// column + "=?", // selection -// ARG1, // , // selection args -// null // sort order -// ); -// } -// } -// failIfFailed(); -// } - -// Temporarily disabled due to taking too much time. -// /** -// * Make sure all URLs are accessible with an order-by. -// */ -// public void testSelectWithSortOrder() { -// for (String[] path : URIs) { -// if (!supportsQuery(path)) continue; -// final Uri uri = getUri(path); -// -// for (String column : getColumns(uri)) { -// checkQueryExecutable(uri, -// null, // projection -// "1=2", // selection -// null, // , // selection args -// column // sort order -// ); -// } -// } -// failIfFailed(); -// } - - /** - * Make sure all URLs are accessible with all arguments. - */ - public void testSelectWithAllArgs() { - for (String[] path : URIs) { - if (!supportsQuery(path)) continue; - final Uri uri = getUri(path); - - final String[] projection = {getColumns(uri)[0]}; - - checkQueryExecutable(uri, - projection, // projection - "1=?", // selection - ARG1, // , // selection args - getColumns(uri)[0] // sort order - ); - } - failIfFailed(); - } - - public void testNonSelect() { - for (String[] path : URIs) { - if (supportsQuery(path)) continue; - final Uri uri = getUri(path); - - checkQueryNotExecutable(uri, // uri - null, // projection - null, // selection - null, // selection args - null // sort order - ); - } - failIfFailed(); - } - - private static boolean supportsTimesContacted(String[] path) { - return path.length > 1 && path[1].contains("s"); - } - - private static boolean supportsTimesUsed(String[] path) { - return path.length > 1 && path[1].contains("t"); - } - - private void checkColumnAccessible(Uri uri, String column) { - try { - try (Cursor c = mResolver.query( - uri, new String[]{column}, column + "=0", null, column - )) { - c.moveToFirst(); - } - } catch (Throwable th) { - addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - } - - /** Test for {@link #checkColumnAccessible} */ - public void testCheckColumnAccessible() { - checkColumnAccessible(Contacts.CONTENT_URI, "x_times_contacted"); - try { - failIfFailed(); - } catch (AssertionFailedError expected) { - return; // expected. - } - fail("Failed to detect issue."); - } - - private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - try { - try (Cursor c = mResolver.query(uri, projection, selection, - selectionArgs, sortOrder)) { - c.moveToFirst(); - } - } catch (IllegalArgumentException th) { - // pass. - return; - } - addFailure("Query on " + uri + - " expected to throw IllegalArgumentException, but succeeded.", null); - } - - private void checkColumnNotAccessible(Uri uri, String column) { - checkColumnNotAccessibleInner(uri, new String[] {column}, null, null, null); - checkColumnNotAccessibleInner(uri, null, column + "=1", null, null); - checkColumnNotAccessibleInner(uri, null, null, null, /* order by */ column); - } - - /** Test for {@link #checkColumnNotAccessible} */ - public void testCheckColumnNotAccessible() { - checkColumnNotAccessible(Contacts.CONTENT_URI, "times_contacted"); - try { - failIfFailed(); - } catch (AssertionFailedError expected) { - return; // expected. - } - fail("Failed to detect issue."); - } - - /** - * Make sure the x_ columns are not accessible. - */ - public void testProhibitedColumns() { - for (String[] path : URIs) { - final Uri uri = getUri(path); - if (supportsTimesContacted(path)) { - checkColumnAccessible(uri, "times_contacted"); - checkColumnAccessible(uri, "last_time_contacted"); - - checkColumnNotAccessible(uri, "X_times_contacted"); - checkColumnNotAccessible(uri, "X_slast_time_contacted"); - } - if (supportsTimesUsed(path)) { - checkColumnAccessible(uri, "times_used"); - checkColumnAccessible(uri, "last_time_used"); - - checkColumnNotAccessible(uri, "X_times_used"); - checkColumnNotAccessible(uri, "X_last_time_used"); - } - } - failIfFailed(); - } - - private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) { - if (shouldWork) { - try { - r.run(); - } catch (Exception e) { - addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e); - } - } else { - try { - r.run(); - addFailure(operation + " for '" + uri + "' NOT failed.", null); - } catch (Exception expected) { - } - } - } - - public void testAllOperations() { - final ContentValues cv = new ContentValues(); - - for (String[] path : URIs) { - final Uri uri = getUri(path); - - try { - cv.clear(); - if (supportsQuery(path)) { - cv.put(getColumns(uri)[0], 1); - } else { - cv.put("_id", 1); - } - if (uri.toString().contains("syncstate")) { - cv.put(SyncState.ACCOUNT_NAME, "abc"); - cv.put(SyncState.ACCOUNT_TYPE, "def"); - } - - checkExecutable("insert", uri, supportsInsert(path), () -> { - final Uri newUri = mResolver.insert(uri, cv); - if (newUri == null) { - addFailure("Insert for '" + uri + "' returned null.", null); - } else { - // "profile/raw_contacts/#" is missing update support. too late to add, so - // just skip. - if (!newUri.toString().startsWith( - "content://com.android.contacts/profile/raw_contacts/")) { - checkExecutable("insert -> update", newUri, true, () -> { - mResolver.update(newUri, cv, null, null); - }); - } - checkExecutable("insert -> delete", newUri, true, () -> { - mResolver.delete(newUri, null, null); - }); - } - }); - checkExecutable("update", uri, supportsUpdate(path), () -> { - mResolver.update(uri, cv, "1=2", null); - }); - checkExecutable("delete", uri, supportsDelete(path), () -> { - mResolver.delete(uri, "1=2", null); - }); - } catch (Throwable th) { - addFailure("Failed: URI=" + uri + " Message=" + th.getMessage(), th); - } - } - failIfFailed(); - } - - public void testAllFileOperations() { - for (String[] path : URIs) { - final Uri uri = getUri(path); - - checkExecutable("openInputStream", uri, supportsRead(path), () -> { - try (InputStream st = mResolver.openInputStream(uri)) { - } catch (FileNotFoundException e) { - // TODO This happens because we try to read nonexistent photos. Ideally - // we should actually check it's readable. - if (e.getMessage().contains("Stream I/O not supported")) { - throw new RuntimeException("Caught Exception: " + e.toString(), e); - } - } catch (Exception e) { - throw new RuntimeException("Caught Exception: " + e.toString(), e); - } - }); - checkExecutable("openOutputStream", uri, supportsWrite(path), () -> { - try (OutputStream st = mResolver.openOutputStream(uri)) { - } catch (Exception e) { - throw new RuntimeException("Caught Exception: " + e.toString(), e); - } - }); - } - failIfFailed(); - } -} - - |