aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp23
-rw-r--r--AndroidManifest.xml12
-rw-r--r--NOTICE190
-rw-r--r--README-tests.md4
-rw-r--r--src/com/android/providers/contacts/AccountWithDataSet.java20
-rw-r--r--src/com/android/providers/contacts/CallLogDatabaseHelper.java76
-rw-r--r--src/com/android/providers/contacts/CallLogProvider.java113
-rw-r--r--src/com/android/providers/contacts/ContactsDatabaseHelper.java405
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java370
-rw-r--r--src/com/android/providers/contacts/DataRowHandler.java14
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForEmail.java7
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java8
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForStructuredName.java16
-rw-r--r--src/com/android/providers/contacts/LegacyApiSupport.java20
-rw-r--r--src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java56
-rw-r--r--src/com/android/providers/contacts/database/MoreDatabaseUtils.java32
-rw-r--r--src/com/android/providers/contacts/util/LogFields.java33
-rw-r--r--src/com/android/providers/contacts/util/LogUtils.java18
-rw-r--r--src/com/android/providers/contacts/util/PhoneAccountHandleMigrationUtils.java219
-rw-r--r--src/com/android/providers/contacts/util/UserUtils.java5
-rw-r--r--test_common/Android.bp8
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/TestUtil.java2
-rw-r--r--tests/Android.bp8
-rw-r--r--tests/assets/phoneAccountHandleMigration/calllog_oldversion.dbbin0 -> 57344 bytes
-rw-r--r--tests/assets/phoneAccountHandleMigration/contacts2_oldversion.dbbin0 -> 352256 bytes
-rw-r--r--tests/assets/upgradeTest/pre_upgrade1600.sql20
-rw-r--r--tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java21
-rw-r--r--tests/src/com/android/providers/contacts/CallLogMigrationTest.java129
-rw-r--r--tests/src/com/android/providers/contacts/CallLogProviderTest.java214
-rw-r--r--tests/src/com/android/providers/contacts/ContactLookupKeyTest.java31
-rw-r--r--tests/src/com/android/providers/contacts/ContactsActor.java4
-rw-r--r--tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java43
-rw-r--r--tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java120
-rw-r--r--tests/src/com/android/providers/contacts/ContactsDatabaseMigrationTest.java110
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2Test.java498
-rw-r--r--tests/src/com/android/providers/contacts/ContextWithServiceOverrides.java52
-rw-r--r--tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java4
-rw-r--r--tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java15
-rw-r--r--tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java3
-rw-r--r--tests2/Android.bp47
-rw-r--r--tests2/AndroidManifest.xml35
-rw-r--r--tests2/AndroidTest.xml27
-rw-r--r--tests2/src/com/android/providers/contacts/tests2/AllUriTest.java730
43 files changed, 2346 insertions, 1416 deletions
diff --git a/Android.bp b/Android.bp
index 4b345e61..08fd9570 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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/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 991413eb..0722d691 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;
@@ -49,6 +53,7 @@ import android.provider.CallLog.Calls;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -73,14 +78,14 @@ 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.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
@@ -93,8 +98,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 + "> ?";
@@ -242,8 +249,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
@@ -259,7 +301,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;
@@ -267,10 +310,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;
@@ -313,6 +358,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");
}
@@ -334,6 +386,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);
}
@@ -861,10 +932,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.
@@ -951,15 +1018,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()) {
@@ -1074,7 +1139,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.
@@ -1114,7 +1178,6 @@ public class CallLogProvider extends ContentProvider {
cursor.close();
}
}
-
}
/**
@@ -1206,16 +1269,34 @@ 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 = 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());
+ 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..368411c7 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,14 +52,6 @@ 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;
public static void log(LogFields logFields) {
@@ -64,10 +61,9 @@ public class LogUtils {
.writeInt(logFields.getUriType())
.writeInt(getCallerType(logFields.isCallerIsSyncAdapter()))
.writeInt(getResultType(logFields.getException()))
+ .writeInt(logFields.getTaskType())
.writeInt(logFields.getResultCount())
.writeLong(getLatencyMicros(logFields.getStartNanos()))
- .writeInt(0) // Empty value for TaskType
- .writeInt(logFields.getMethodCall())
.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
new file mode 100644
index 00000000..e00b9855
--- /dev/null
+++ b/tests/assets/phoneAccountHandleMigration/calllog_oldversion.db
Binary files differ
diff --git a/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db b/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db
new file mode 100644
index 00000000..128da646
--- /dev/null
+++ b/tests/assets/phoneAccountHandleMigration/contacts2_oldversion.db
Binary files differ
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..c7555823 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -26,6 +26,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 +81,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 +103,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 +123,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 +137,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 +446,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 +470,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);
}
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();
- }
-}
-
-