Merge "Merge 24Q4 into AOSP main" into main
diff --git a/Android.bp b/Android.bp
index 945b32c..17f5d8e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,11 +12,12 @@
"src/com/android/providers/contacts/EventLogTags.logtags",
],
libs: [
- "ext"
+ "ext",
],
static_libs: [
"android-common",
"com.android.vcard",
+ "contactsprovider_flags_java_lib",
"guava",
"android.content.pm.flags-aconfig-java",
],
@@ -41,3 +42,15 @@
name: "contacts-provider-platform-compat-config",
src: ":ContactsProvider",
}
+
+aconfig_declarations {
+ name: "contactsprovider_flags",
+ package: "com.android.providers.contacts.flags",
+ container: "system",
+ srcs: ["contactsprovider_flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "contactsprovider_flags_java_lib",
+ aconfig_declarations: "contactsprovider_flags",
+}
diff --git a/contactsprovider_flags.aconfig b/contactsprovider_flags.aconfig
new file mode 100644
index 0000000..7eee666
--- /dev/null
+++ b/contactsprovider_flags.aconfig
@@ -0,0 +1,27 @@
+package: "com.android.providers.contacts.flags"
+container: "system"
+
+flag {
+ name: "cp2_account_move_flag"
+ namespace: "contacts"
+ description: "Methods for bulk move of contacts between accounts"
+ bug: "330324156"
+}
+flag {
+ name: "cp2_account_move_sync_stub_flag"
+ namespace: "contacts"
+ description: "Methods for writing sync stubs during bulk move of contacts between accounts"
+ bug: "330324156"
+}
+flag {
+ name: "enable_new_default_account_rule_flag"
+ namespace: "contacts"
+ description: "Enable new default account for contacts"
+ bug: "337979000"
+}
+flag {
+ name: "cp2_sync_search_index_flag"
+ namespace: "contacts"
+ description: "Refactor to update search index during account removal and contact aggregation"
+ bug: "363260703"
+}
diff --git a/res/values-af/arrays.xml b/res/values-af/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-af/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-am/arrays.xml b/res/values-am/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-am/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ar/arrays.xml b/res/values-ar/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ar/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-as/arrays.xml b/res/values-as/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-as/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-az/arrays.xml b/res/values-az/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-az/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-b+sr+Latn/arrays.xml b/res/values-b+sr+Latn/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-b+sr+Latn/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-be/arrays.xml b/res/values-be/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-be/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-bg/arrays.xml b/res/values-bg/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-bg/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-bn/arrays.xml b/res/values-bn/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-bn/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-bs/arrays.xml b/res/values-bs/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-bs/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ca/arrays.xml b/res/values-ca/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ca/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-cs/arrays.xml b/res/values-cs/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-cs/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-da/arrays.xml b/res/values-da/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-da/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-de/arrays.xml b/res/values-de/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-de/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-el/arrays.xml b/res/values-el/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-el/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-en-rAU/arrays.xml b/res/values-en-rAU/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-en-rAU/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-en-rCA/arrays.xml b/res/values-en-rCA/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-en-rCA/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-en-rGB/arrays.xml b/res/values-en-rGB/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-en-rGB/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-en-rIN/arrays.xml b/res/values-en-rIN/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-en-rIN/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-en-rXC/arrays.xml b/res/values-en-rXC/arrays.xml
new file mode 100644
index 0000000..9df5737
--- /dev/null
+++ b/res/values-en-rXC/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-es-rUS/arrays.xml b/res/values-es-rUS/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-es-rUS/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-es/arrays.xml b/res/values-es/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-es/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-et/arrays.xml b/res/values-et/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-et/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-eu/arrays.xml b/res/values-eu/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-eu/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-fa/arrays.xml b/res/values-fa/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-fa/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-fi/arrays.xml b/res/values-fi/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-fi/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-fr-rCA/arrays.xml b/res/values-fr-rCA/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-fr-rCA/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-fr/arrays.xml b/res/values-fr/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-fr/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-gl/arrays.xml b/res/values-gl/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-gl/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-gu/arrays.xml b/res/values-gu/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-gu/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-hi/arrays.xml b/res/values-hi/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-hi/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-hr/arrays.xml b/res/values-hr/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-hr/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-hu/arrays.xml b/res/values-hu/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-hu/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-hy/arrays.xml b/res/values-hy/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-hy/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-in/arrays.xml b/res/values-in/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-in/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-is/arrays.xml b/res/values-is/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-is/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-it/arrays.xml b/res/values-it/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-it/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-iw/arrays.xml b/res/values-iw/arrays.xml
new file mode 100644
index 0000000..cd38f26
--- /dev/null
+++ b/res/values-iw/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ja/arrays.xml b/res/values-ja/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ja/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ka/arrays.xml b/res/values-ka/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ka/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-kk/arrays.xml b/res/values-kk/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-kk/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-km/arrays.xml b/res/values-km/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-km/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-kn/arrays.xml b/res/values-kn/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-kn/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ko/arrays.xml b/res/values-ko/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ko/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ky/arrays.xml b/res/values-ky/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ky/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-lo/arrays.xml b/res/values-lo/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-lo/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-lt/arrays.xml b/res/values-lt/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-lt/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-lv/arrays.xml b/res/values-lv/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-lv/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-mk/arrays.xml b/res/values-mk/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-mk/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ml/arrays.xml b/res/values-ml/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ml/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-mn/arrays.xml b/res/values-mn/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-mn/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-mr/arrays.xml b/res/values-mr/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-mr/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ms/arrays.xml b/res/values-ms/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ms/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-my/arrays.xml b/res/values-my/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-my/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-nb/arrays.xml b/res/values-nb/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-nb/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ne/arrays.xml b/res/values-ne/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ne/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-nl/arrays.xml b/res/values-nl/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-nl/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-or/arrays.xml b/res/values-or/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-or/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-pa/arrays.xml b/res/values-pa/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-pa/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-pl/arrays.xml b/res/values-pl/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-pl/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-pt-rBR/arrays.xml b/res/values-pt-rBR/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-pt-rBR/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-pt-rPT/arrays.xml b/res/values-pt-rPT/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-pt-rPT/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-pt/arrays.xml b/res/values-pt/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-pt/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ro/arrays.xml b/res/values-ro/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ro/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ru/arrays.xml b/res/values-ru/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ru/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-si/arrays.xml b/res/values-si/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-si/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sk/arrays.xml b/res/values-sk/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sk/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sl/arrays.xml b/res/values-sl/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sl/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sq/arrays.xml b/res/values-sq/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sq/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sr/arrays.xml b/res/values-sr/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sr/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sv/arrays.xml b/res/values-sv/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sv/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-sw/arrays.xml b/res/values-sw/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-sw/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ta/arrays.xml b/res/values-ta/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ta/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-te/arrays.xml b/res/values-te/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-te/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-th/arrays.xml b/res/values-th/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-th/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-tl/arrays.xml b/res/values-tl/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-tl/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-tr/arrays.xml b/res/values-tr/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-tr/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-uk/arrays.xml b/res/values-uk/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-uk/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-ur/arrays.xml b/res/values-ur/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-ur/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-uz/arrays.xml b/res/values-uz/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-uz/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-vi/arrays.xml b/res/values-vi/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-vi/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-zh-rCN/arrays.xml b/res/values-zh-rCN/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-zh-rCN/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-zh-rHK/arrays.xml b/res/values-zh-rHK/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-zh-rHK/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-zh-rTW/arrays.xml b/res/values-zh-rTW/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-zh-rTW/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/res/values-zu/arrays.xml b/res/values-zu/arrays.xml
new file mode 100644
index 0000000..944e203
--- /dev/null
+++ b/res/values-zu/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="eligible_system_cloud_account_types">
+ <item msgid="7130475166467776698">"com.google"</item>
+ </string-array>
+</resources>
diff --git a/src/com/android/providers/contacts/AccountResolver.java b/src/com/android/providers/contacts/AccountResolver.java
new file mode 100644
index 0000000..5372cf0
--- /dev/null
+++ b/src/com/android/providers/contacts/AccountResolver.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2024 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.accounts.Account;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.SimAccount;
+import android.text.TextUtils;
+
+import com.android.providers.contacts.DefaultAccount.AccountCategory;
+
+import java.util.List;
+
+public class AccountResolver {
+ private static final String TAG = "AccountResolver";
+
+ private final ContactsDatabaseHelper mDbHelper;
+ private final DefaultAccountManager mDefaultAccountManager;
+
+ public AccountResolver(ContactsDatabaseHelper dbHelper,
+ DefaultAccountManager defaultAccountManager) {
+ mDbHelper = dbHelper;
+ mDefaultAccountManager = defaultAccountManager;
+ }
+
+ /**
+ * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
+ * in the URI or values (if any).
+ * @param uri Current {@link Uri} being operated on.
+ * @param values {@link ContentValues} to read and possibly update.
+ * @param applyDefaultAccount Whether to look up default account during account resolution.
+ */
+ public AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values,
+ boolean applyDefaultAccount) {
+ final Account[] accounts = resolveAccount(uri, values);
+ final Account account = applyDefaultAccount
+ ? getAccountWithDefaultAccountApplied(uri, accounts)
+ : getFirstAccountOrNull(accounts);
+
+ AccountWithDataSet accountWithDataSet = null;
+ if (account != null) {
+ String dataSet = ContactsProvider2.getQueryParameter(uri, RawContacts.DATA_SET);
+ if (dataSet == null) {
+ dataSet = values.getAsString(RawContacts.DATA_SET);
+ } else {
+ values.put(RawContacts.DATA_SET, dataSet);
+ }
+ accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
+ }
+
+ return accountWithDataSet;
+ }
+
+ /**
+ * Resolves the account to be used, taking into consideration the default account settings.
+ *
+ * @param accounts 1-size array which contains specified account, or empty array if account is
+ * not specified.
+ * @param uri The URI used for resolving accounts.
+ * @return The resolved account, or null if it's the default device (aka "NULL") account.
+ * @throws IllegalArgumentException If there's an issue with the account resolution due to
+ * default account incompatible account types.
+ */
+ private Account getAccountWithDefaultAccountApplied(Uri uri, Account[] accounts)
+ throws IllegalArgumentException {
+ if (accounts.length == 0) {
+ DefaultAccount defaultAccount = mDefaultAccountManager.pullDefaultAccount();
+ if (defaultAccount.getAccountCategory() == AccountCategory.UNKNOWN) {
+ String exceptionMessage = mDbHelper.exceptionMessage(
+ "Must specify ACCOUNT_NAME and ACCOUNT_TYPE",
+ uri);
+ throw new IllegalArgumentException(exceptionMessage);
+ } else if (defaultAccount.getAccountCategory() == AccountCategory.DEVICE) {
+ return getLocalAccount();
+ } else {
+ return defaultAccount.getCloudAccount();
+ }
+ } else {
+ checkAccountIsWritableInternal(accounts[0]);
+ return accounts[0];
+ }
+ }
+
+ /**
+ * Checks if the specified account is writable.
+ *
+ * <p>This method verifies if contacts can be written to the given account based on the
+ * current default account settings. It throws an {@link IllegalArgumentException} if
+ * the account is not writable.</p>
+ *
+ * @param accountName The name of the account to check.
+ * @param accountType The type of the account to check.
+ *
+ * @throws IllegalArgumentException if either of the following conditions are met:
+ * <ul>
+ * <li>Only one of <code>accountName</code> or <code>accountType</code> is
+ * specified.</li>
+ * <li>The default account is set to cloud and the specified account is a local
+ * (device or SIM) account.</li>
+ * </ul>
+ */
+ public void checkAccountIsWritable(String accountName, String accountType) {
+ if (TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType)) {
+ throw new IllegalArgumentException(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE");
+ }
+ if (TextUtils.isEmpty(accountName)) {
+ checkAccountIsWritableInternal(/*account=*/null);
+ } else {
+ checkAccountIsWritableInternal(new Account(accountName, accountType));
+ }
+ }
+
+ private void checkAccountIsWritableInternal(Account account)
+ throws IllegalArgumentException {
+ DefaultAccount defaultAccount = mDefaultAccountManager.pullDefaultAccount();
+
+ if (defaultAccount.getAccountCategory() == AccountCategory.CLOUD) {
+ if (isDeviceOrSimAccount(account)) {
+ throw new IllegalArgumentException("Cannot write contacts to local accounts "
+ + "when default account is set to cloud");
+ }
+ }
+ }
+
+ private static Account getLocalAccount() {
+ if (TextUtils.isEmpty(AccountWithDataSet.LOCAL.getAccountName())) {
+ // AccountWithDataSet.LOCAL's getAccountType() must be null as well, thus we return
+ // the NULL account.
+ return null;
+ } else {
+ // AccountWithDataSet.LOCAL's getAccountType() must not be null as well, thus we return
+ // the customized local account.
+ return new Account(AccountWithDataSet.LOCAL.getAccountName(),
+ AccountWithDataSet.LOCAL.getAccountType());
+ }
+ }
+
+ /**
+ * Gets the first account from the array, or null if the array is empty.
+ *
+ * @param accounts The array of accounts.
+ * @return The first account, or null if the array is empty.
+ */
+ private Account getFirstAccountOrNull(Account[] accounts) {
+ return accounts.length > 0 ? accounts[0] : null;
+ }
+
+
+ private boolean isDeviceOrSimAccount(Account account) {
+ AccountWithDataSet accountWithDataSet = account == null
+ ? new AccountWithDataSet(null, null, null)
+ : new AccountWithDataSet(account.name, account.type, null);
+
+ List<SimAccount> simAccounts = mDbHelper.getAllSimAccounts();
+ return accountWithDataSet.isLocalAccount() || accountWithDataSet.inSimAccounts(simAccounts);
+ }
+
+ /**
+ * If account is non-null then store it in the values. If the account is
+ * already specified in the values then it must be consistent with the
+ * account, if it is non-null.
+ *
+ * @param uri Current {@link Uri} being operated on.
+ * @param values {@link ContentValues} to read and possibly update.
+ * @return 1-size array which contains account specified by {@link Uri} and
+ * {@link ContentValues}, or empty array if account is not specified.
+ * @throws IllegalArgumentException when only one of
+ * {@link RawContacts#ACCOUNT_NAME} or
+ * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
+ * other undefined.
+ * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
+ * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
+ * the given {@link Uri} and {@link ContentValues}.
+ */
+ private Account[] resolveAccount(Uri uri, ContentValues values)
+ throws IllegalArgumentException {
+ String accountName = ContactsProvider2.getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ String accountType = ContactsProvider2.getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+ final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+
+ if (accountName == null && accountType == null
+ && !values.containsKey(RawContacts.ACCOUNT_NAME)
+ && !values.containsKey(RawContacts.ACCOUNT_TYPE)) {
+ // Account is not specified.
+ return new Account[0];
+ }
+
+ String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+ String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+
+ final boolean partialValues = TextUtils.isEmpty(valueAccountName)
+ ^ TextUtils.isEmpty(valueAccountType);
+
+ if (partialUri || partialValues) {
+ // Throw when either account is incomplete.
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
+ }
+
+ // Accounts are valid by only checking one parameter, since we've
+ // already ruled out partial accounts.
+ final boolean validUri = !TextUtils.isEmpty(accountName);
+ final boolean validValues = !TextUtils.isEmpty(valueAccountName);
+
+ if (validValues && validUri) {
+ // Check that accounts match when both present
+ final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
+ && TextUtils.equals(accountType, valueAccountType);
+ if (!accountMatch) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
+ }
+ } else if (validUri) {
+ // Fill values from the URI when not present.
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ } else if (validValues) {
+ accountName = valueAccountName;
+ accountType = valueAccountType;
+ } else {
+ return new Account[]{null};
+ }
+
+ return new Account[]{new Account(accountName, accountType)};
+ }
+}
diff --git a/src/com/android/providers/contacts/CallComposerLocationProvider.java b/src/com/android/providers/contacts/CallComposerLocationProvider.java
index 568a189..49a2a09 100644
--- a/src/com/android/providers/contacts/CallComposerLocationProvider.java
+++ b/src/com/android/providers/contacts/CallComposerLocationProvider.java
@@ -18,7 +18,6 @@
import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
-import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentProvider;
@@ -33,12 +32,13 @@
import android.net.Uri;
import android.os.Binder;
import android.os.Process;
+import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.Log;
-
+import com.android.internal.telephony.TelephonyPermissions;
import com.android.providers.contacts.util.SelectionBuilder;
import java.util.Objects;
@@ -173,7 +173,8 @@
private void enforceAccessRestrictions() {
int uid = Binder.getCallingUid();
- if (uid == Process.SYSTEM_UID || uid == Process.myUid() || uid == Process.PHONE_UID) {
+ if (TelephonyPermissions.isSystemOrPhone(uid)
+ || UserHandle.isSameApp(uid, Process.myUid())) {
return;
}
String defaultDialerPackageName = getContext().getSystemService(TelecomManager.class)
diff --git a/src/com/android/providers/contacts/ContactMover.java b/src/com/android/providers/contacts/ContactMover.java
new file mode 100644
index 0000000..2ab7e8f
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactMover.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.flags.Flags.cp2AccountMoveFlag;
+import static com.android.providers.contacts.flags.Flags.cp2AccountMoveSyncStubFlag;
+
+import android.accounts.Account;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.providers.contacts.util.NeededForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A class to move {@link RawContacts} and {@link Groups} from one account to another.
+ */
+@NeededForTesting
+public class ContactMover {
+ private static final String TAG = "ContactMover";
+ private final ContactsDatabaseHelper mDbHelper;
+ private final ContactsProvider2 mCp2;
+ private final DefaultAccountManager mDefaultAccountManager;
+
+ @NeededForTesting
+ public ContactMover(ContactsProvider2 contactsProvider,
+ ContactsDatabaseHelper contactsDatabaseHelper,
+ DefaultAccountManager defaultAccountManager) {
+ mCp2 = contactsProvider;
+ mDbHelper = contactsDatabaseHelper;
+ mDefaultAccountManager = defaultAccountManager;
+ }
+
+ private void updateRawContactsAccount(
+ AccountWithDataSet destAccount, Set<Long> rawContactIds) {
+ if (rawContactIds.isEmpty()) {
+ return;
+ }
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, destAccount.getAccountName());
+ values.put(RawContacts.ACCOUNT_TYPE, destAccount.getAccountType());
+ values.put(RawContacts.DATA_SET, destAccount.getDataSet());
+ values.putNull(RawContacts.SOURCE_ID);
+ values.putNull(RawContacts.SYNC1);
+ values.putNull(RawContacts.SYNC2);
+ values.putNull(RawContacts.SYNC3);
+ values.putNull(RawContacts.SYNC4);
+
+ // actually update the account columns and break the source ID
+ mCp2.update(
+ RawContacts.CONTENT_URI,
+ values,
+ RawContacts._ID + " IN (" + TextUtils.join(",", rawContactIds) + ")",
+ new String[] {});
+ }
+
+ private void updateGroupAccount(
+ AccountWithDataSet destAccount, Set<Long> groupIds) {
+ if (groupIds.isEmpty()) {
+ return;
+ }
+ ContentValues values = new ContentValues();
+ values.put(Groups.ACCOUNT_NAME, destAccount.getAccountName());
+ values.put(Groups.ACCOUNT_TYPE, destAccount.getAccountType());
+ values.put(Groups.DATA_SET, destAccount.getDataSet());
+ values.putNull(Groups.SOURCE_ID);
+ values.putNull(Groups.SYNC1);
+ values.putNull(Groups.SYNC2);
+ values.putNull(Groups.SYNC3);
+ values.putNull(Groups.SYNC4);
+
+ // actually update the account columns and break the source ID
+ mCp2.update(
+ Groups.CONTENT_URI,
+ values,
+ Groups._ID + " IN (" + TextUtils.join(",", groupIds) + ")",
+ new String[] {});
+ }
+
+ private void updateGroupDataRows(Map<Long, Long> groupIdMap) {
+ // for each group in the groupIdMap, update all Group Membership data rows from key to value
+ for (Map.Entry<Long, Long> groupIds: groupIdMap.entrySet()) {
+ mDbHelper.updateGroupMemberships(groupIds.getKey(), groupIds.getValue());
+ }
+
+ }
+
+ private boolean isAccountTypeMatch(
+ AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) {
+ if (sourceAccount.getAccountType() == null) {
+ return destAccount.getAccountType() == null;
+ }
+
+ return sourceAccount.getAccountType().equals(destAccount.getAccountType());
+ }
+
+ private boolean isDataSetMatch(
+ AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) {
+ if (sourceAccount.getDataSet() == null) {
+ return destAccount.getDataSet() == null;
+ }
+
+ return sourceAccount.getDataSet().equals(destAccount.getDataSet());
+ }
+
+ private void moveNonSystemGroups(AccountWithDataSet sourceAccount,
+ AccountWithDataSet destAccount, boolean insertSyncStubs) {
+ Pair<Set<Long>, Map<Long, Long>> nonSystemGroups = mDbHelper
+ .deDuplicateGroups(sourceAccount, destAccount, /* isSystemGroupQuery= */ false);
+ Set<Long> nonSystemUniqueGroups = nonSystemGroups.first;
+ Map<Long, Long> nonSystemDuplicateGroupMap = nonSystemGroups.second;
+
+ // For non-system groups that are duplicated in source and dest:
+ // 1. update contact data rows (to point do the group in dest)
+ // 2. Set deleted = 1 for dupe groups in source
+ updateGroupDataRows(nonSystemDuplicateGroupMap);
+ for (Map.Entry<Long, Long> groupIds: nonSystemDuplicateGroupMap.entrySet()) {
+ mCp2.deleteGroup(Groups.CONTENT_URI, groupIds.getKey(), false);
+ }
+
+ // For non-system groups that only exist in source:
+ // 1. Write sync stubs for synced groups (if needed)
+ // 2. Update account ids
+ if (!sourceAccount.isLocalAccount() && insertSyncStubs) {
+ mDbHelper.insertGroupSyncStubs(sourceAccount, nonSystemUniqueGroups);
+ }
+ updateGroupAccount(destAccount, nonSystemUniqueGroups);
+ }
+
+ private void moveSystemGroups(
+ AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) {
+ Pair<Set<Long>, Map<Long, Long>> systemGroups = mDbHelper
+ .deDuplicateGroups(sourceAccount, destAccount, /* isSystemGroupQuery= */ true);
+ Set<Long> systemUniqueGroups = systemGroups.first;
+ Map<Long, Long> systemDuplicateGroupMap = systemGroups.second;
+
+ // For system groups in source that have a match in dest:
+ // 1. Update contact data rows (can't delete the existing groups)
+ updateGroupDataRows(systemDuplicateGroupMap);
+
+ // For system groups that only exist in source:
+ // 1. Get content values for the relevant (non-empty) groups
+ // 2. Create a group in destination account (while building an ID map)
+ // 3. Update contact data rows to point at the new group(s)
+ Map<Long, ContentValues> oldIdToNewValues = mDbHelper
+ .getGroupContentValuesForMoveCopy(destAccount, systemUniqueGroups);
+ Map<Long, Long> systemGroupIdMap = new HashMap<>();
+ for (Map.Entry<Long, ContentValues> idToValues: oldIdToNewValues.entrySet()) {
+ Uri newGroupUri = mCp2.insert(Groups.CONTENT_URI, idToValues.getValue());
+ if (newGroupUri != null) {
+ Long newGroupId = ContentUris.parseId(newGroupUri);
+ systemGroupIdMap.put(idToValues.getKey(), newGroupId);
+ }
+ }
+ updateGroupDataRows(systemGroupIdMap);
+
+ // now delete membership data rows for any unique groups we skipped - otherwise the contacts
+ // will be left with data rows pointing to the skipped groups in the source account.
+ if (!oldIdToNewValues.isEmpty()) {
+ systemUniqueGroups.removeAll(oldIdToNewValues.keySet());
+ }
+ mCp2.delete(Data.CONTENT_URI,
+ CommonDataKinds.GroupMembership.GROUP_ROW_ID
+ + " IN (" + TextUtils.join(",", systemUniqueGroups) + ")"
+ + " AND " + Data.MIMETYPE + " = ?",
+ new String[] {CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE}
+ );
+ }
+
+ private void moveGroups(AccountWithDataSet sourceAccount, AccountWithDataSet destAccount,
+ boolean createSyncStubs) {
+ moveNonSystemGroups(sourceAccount, destAccount, createSyncStubs);
+ moveSystemGroups(sourceAccount, destAccount);
+ }
+
+ private Set<AccountWithDataSet> getLocalAccounts() {
+ AccountWithDataSet nullAccount = new AccountWithDataSet(
+ /* accountName= */ null, /* accountType= */ null, /* dataSet= */ null);
+ if (AccountWithDataSet.LOCAL.equals(nullAccount)) {
+ return Set.of(AccountWithDataSet.LOCAL);
+ }
+ return Set.of(
+ AccountWithDataSet.LOCAL,
+ nullAccount);
+ }
+
+ private Set<AccountWithDataSet> getSimAccounts() {
+ return mDbHelper.getAllSimAccounts().stream()
+ .map(simAccount ->
+ new AccountWithDataSet(
+ simAccount.getAccountName(), simAccount.getAccountType(), null))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Moves {@link RawContacts} and {@link Groups} from the local account(s) to the Cloud Default
+ * Account (if any).
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ void moveLocalToCloudDefaultAccount() {
+ if (!cp2AccountMoveFlag()) {
+ Log.w(TAG, "moveLocalToCloudDefaultAccount: flag disabled");
+ return;
+ }
+
+ // Check if there is a cloud default account set
+ // - if not, then we don't need to do anything
+ // - if there is, then that's our destAccount, get the AccountWithDataSet
+ Account account = mDefaultAccountManager.pullDefaultAccount().getCloudAccount();
+ if (account == null) {
+ Log.w(TAG, "moveToDefaultCloudAccount with no default cloud account set");
+ return;
+ }
+ AccountWithDataSet destAccount = new AccountWithDataSet(
+ account.name, account.type, /* dataSet= */ null);
+
+ // Move any contacts from the local account to the destination account
+ moveRawContacts(getLocalAccounts(), destAccount);
+ }
+
+ /**
+ * Moves {@link RawContacts} and {@link Groups} from the SIM account(s) to the Cloud Default
+ * Account (if any).
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ void moveSimToCloudDefaultAccount() {
+ if (!cp2AccountMoveFlag()) {
+ Log.w(TAG, "moveLocalToCloudDefaultAccount: flag disabled");
+ return;
+ }
+
+ // Check if there is a cloud default account set
+ // - if not, then we don't need to do anything
+ // - if there is, then that's our destAccount, get the AccountWithDataSet
+ Account account = mDefaultAccountManager.pullDefaultAccount().getCloudAccount();
+ if (account == null) {
+ Log.w(TAG, "moveToDefaultCloudAccount with no default cloud account set");
+ return;
+ }
+ AccountWithDataSet destAccount = new AccountWithDataSet(
+ account.name, account.type, /* dataSet= */ null);
+
+ // Move any contacts from the sim accounts to the destination account
+ moveRawContacts(getSimAccounts(), destAccount);
+ }
+
+ /**
+ * Gets the number of {@link RawContacts} in the local account(s) which may be moved using
+ * {@link ContactMover#moveLocalToCloudDefaultAccount} (if any).
+ * @return the number of {@link RawContacts} in the local account(s), or 0 if there is no Cloud
+ * Default Account.
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ int getNumberLocalContacts() {
+ if (!cp2AccountMoveFlag()) {
+ Log.w(TAG, "getNumberLocalContacts: flag disabled");
+ return 0;
+ }
+
+ // Check if there is a cloud default account set
+ // - if not, then we don't need to do anything, count = 0
+ // - if there is, then do the count
+ Account account = mDefaultAccountManager.pullDefaultAccount().getCloudAccount();
+ if (account == null) {
+ Log.w(TAG, "getNumberLocalContacts with no default cloud account set");
+ return 0;
+ }
+
+ // Count any contacts in the local account(s)
+ return countRawContactsForAccounts(getLocalAccounts());
+ }
+
+ /**
+ * Gets the number of {@link RawContacts} in the SIM account(s) which may be moved using
+ * {@link ContactMover#moveSimToCloudDefaultAccount} (if any).
+ * @return the number of {@link RawContacts} in the SIM account(s), or 0 if there is no Cloud
+ * Default Account.
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ int getNumberSimContacts() {
+ if (!cp2AccountMoveFlag()) {
+ Log.w(TAG, "getNumberSimContacts: flag disabled");
+ return 0;
+ }
+
+ // Check if there is a cloud default account set
+ // - if not, then we don't need to do anything, count = 0
+ // - if there is, then do the count
+ Account account = mDefaultAccountManager.pullDefaultAccount().getCloudAccount();
+ if (account == null) {
+ Log.w(TAG, "getNumberSimContacts with no default cloud account set");
+ return 0;
+ }
+
+ // Count any contacts in the sim accounts.
+ return countRawContactsForAccounts(getSimAccounts());
+ }
+
+ /**
+ * Moves {@link RawContacts} and {@link Groups} from one account to another.
+ * @param sourceAccounts the source {@link AccountWithDataSet}s to move contacts and groups
+ * from.
+ * @param destAccount the destination {@link AccountWithDataSet} to move contacts and groups to.
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ void moveRawContacts(Set<AccountWithDataSet> sourceAccounts, AccountWithDataSet destAccount) {
+ if (!cp2AccountMoveFlag()) {
+ Log.w(TAG, "moveRawContacts: flag disabled");
+ return;
+ }
+ moveRawContactsForAccounts(
+ sourceAccounts, destAccount, /* insertSyncStubs= */ false);
+ }
+
+ /**
+ * Moves {@link RawContacts} and {@link Groups} from one account to another, while writing sync
+ * stubs in the source account to notify relevant sync adapters in the source account of the
+ * move.
+ * @param sourceAccounts the source {@link AccountWithDataSet}s to move contacts and groups
+ * from.
+ * @param destAccount the destination {@link AccountWithDataSet} to move contacts and groups to.
+ */
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ void moveRawContactsWithSyncStubs(Set<AccountWithDataSet> sourceAccounts,
+ AccountWithDataSet destAccount) {
+ if (!cp2AccountMoveFlag() || !cp2AccountMoveSyncStubFlag()) {
+ Log.w(TAG, "moveRawContactsWithSyncStubs: flags disabled");
+ return;
+ }
+ moveRawContactsForAccounts(sourceAccounts, destAccount, /* insertSyncStubs= */ true);
+ }
+
+ private int countRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts) {
+ return mDbHelper.countRawContactsQuery(sourceAccounts);
+ }
+
+ private void moveRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts,
+ AccountWithDataSet destAccount, boolean insertSyncStubs) {
+ if (sourceAccounts.contains(destAccount)) {
+ throw new IllegalArgumentException("Source and destination accounts must differ");
+ }
+
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ for (AccountWithDataSet source: sourceAccounts) {
+ moveRawContactsInternal(source, destAccount, insertSyncStubs);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void moveRawContactsInternal(AccountWithDataSet sourceAccount,
+ AccountWithDataSet destAccount, boolean insertSyncStubs) {
+ // If we are moving between account types or data sets, delete non-portable data rows
+ // from the source
+ if (!isAccountTypeMatch(sourceAccount, destAccount)
+ || !isDataSetMatch(sourceAccount, destAccount)) {
+ mDbHelper.deleteNonCommonDataRows(sourceAccount);
+ }
+
+ // Move any groups and group memberships from the source to destination account
+ moveGroups(sourceAccount, destAccount, insertSyncStubs);
+
+ // Next, compare raw contacts from source and destination accounts, find the unique
+ // raw contacts from source account;
+ Pair<Set<Long>, Set<Long>> sourceRawContactIds =
+ mDbHelper.deDuplicateRawContacts(sourceAccount, destAccount);
+ Set<Long> nonDuplicates = sourceRawContactIds.first;
+ Set<Long> duplicates = sourceRawContactIds.second;
+
+ if (!sourceAccount.isLocalAccount() && insertSyncStubs) {
+ /*
+ If the source account isn't a device account, and we want to write stub contacts
+ for the move, create them now.
+ This ensures any sync adapters on the source account won't just sync the moved
+ contacts back down (creating duplicates).
+ */
+ mDbHelper.insertRawContactSyncStubs(sourceAccount, nonDuplicates);
+ }
+
+ // move the contacts to the destination account
+ updateRawContactsAccount(destAccount, nonDuplicates);
+
+ // Last, clear the duplicates.
+ // Since these are duplicates, we don't need to do anything else with them
+ for (long rawContactId: duplicates) {
+ mCp2.deleteRawContact(
+ rawContactId,
+ mDbHelper.getContactId(rawContactId),
+ false);
+ }
+ }
+
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 9446462..eabadd4 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -43,7 +43,6 @@
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;
@@ -90,13 +89,12 @@
import android.util.ArraySet;
import android.util.Base64;
import android.util.Log;
+import android.util.Pair;
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;
import com.android.providers.contacts.database.ContactsTableUtil;
import com.android.providers.contacts.database.DeletedContactsTableUtil;
import com.android.providers.contacts.database.MoreDatabaseUtils;
@@ -107,21 +105,22 @@
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.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
/**
* Database helper for contacts. Designed as a singleton to make sure that all
@@ -952,7 +951,7 @@
*/
private long mDatabaseCreationTime;
- private MessageDigest mMessageDigest;
+ private final MessageDigest mMessageDigest;
{
try {
mMessageDigest = MessageDigest.getInstance("SHA-1");
@@ -4247,11 +4246,23 @@
}
/**
+ * Clear the previous set default account from Accounts table.
+ */
+ public void clearDefaultAccount() {
+ SQLiteDatabase db = getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(AccountsColumns.IS_DEFAULT, 0);
+
+ db.update(Tables.ACCOUNTS, values, null, null);
+ }
+
+ /**
* 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.
+ * @throws IllegalArgumentException if one of the account name or type is null, but not both.
*/
public void setDefaultAccount(String accountName, String accountType) {
if (TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType)) {
@@ -4281,9 +4292,11 @@
/**
* Return the default account from Accounts table.
+ *
+ * @return empty array if Default account is not set; 1-element with null if the default account
+ * is set to NULL account; 1-element with non-null account otherwise.
*/
- public Account getDefaultAccount() {
- Account defaultAccount = null;
+ public Account[] getDefaultAccountIfAny() {
try (Cursor c = getReadableDatabase().rawQuery(
"SELECT " + AccountsColumns.ACCOUNT_NAME + ","
+ AccountsColumns.ACCOUNT_TYPE + " FROM " + Tables.ACCOUNTS + " WHERE "
@@ -4291,12 +4304,14 @@
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);
+ if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+ return new Account[]{null};
+ } else {
+ return new Account[]{new Account(accountName, accountType)};
}
}
}
- return defaultAccount;
+ return new Account[0];
}
/**
@@ -4395,32 +4410,70 @@
* If {@code optionalContactId} is non-negative, it'll update only for the specified contact.
*/
private void updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId) {
- final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
- String[] selectionArgs = new String[] {String.valueOf(groupMembershipMimetypeId)};
+ // NOTE: This requires late binding of GroupMembership MIME-type
+ final String contactIsVisible = """
+ SELECT
+ MAX((SELECT (CASE WHEN
+ (CASE
+ WHEN COUNT(groups._id)=0
+ THEN ungrouped_visible
+ ELSE MAX(group_visible)
+ END)=1 THEN 1 ELSE 0 END)
+ FROM raw_contacts JOIN accounts ON
+ (raw_contacts.account_id = accounts._id)
+ LEFT OUTER JOIN data ON (data.mimetype_id=? AND
+ data.raw_contact_id = raw_contacts._id)
+ LEFT OUTER JOIN groups ON (groups._id = data.data1)
+ WHERE raw_contacts._id = outer_raw_contacts._id))
+ FROM raw_contacts AS outer_raw_contacts
+ WHERE contact_id = contacts._id
+ GROUP BY contact_id
+ """;
- final String contactIdSelect = (optionalContactId < 0) ? "" :
- (Contacts._ID + "=" + optionalContactId + " AND ");
+ final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
// First delete what needs to be deleted, then insert what needs to be added.
// Since flash writes are very expensive, this approach is much better than
// delete-all-insert-all.
- db.execSQL(
- "DELETE FROM " + Tables.VISIBLE_CONTACTS +
- " WHERE " + Contacts._ID + " IN" +
- "(SELECT " + Contacts._ID +
- " FROM " + Tables.CONTACTS +
- " WHERE " + contactIdSelect + "(" + Clauses.CONTACT_IS_VISIBLE + ")=0) ",
- selectionArgs);
+ if (optionalContactId < 0) {
+ String[] selectionArgs = new String[] {String.valueOf(groupMembershipMimetypeId)};
+ db.execSQL("""
+ DELETE FROM visible_contacts
+ WHERE _id IN
+ (SELECT contacts._id
+ FROM contacts
+ WHERE (""" + contactIsVisible + ")=0)",
+ selectionArgs);
- db.execSQL(
- "INSERT INTO " + Tables.VISIBLE_CONTACTS +
- " SELECT " + Contacts._ID +
- " FROM " + Tables.CONTACTS +
- " WHERE " +
- contactIdSelect +
- Contacts._ID + " NOT IN " + Tables.VISIBLE_CONTACTS +
- " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 ",
- selectionArgs);
+ db.execSQL("""
+ INSERT INTO visible_contacts
+ SELECT _id
+ FROM contacts
+ WHERE _id NOT IN visible_contacts
+ AND (""" + contactIsVisible + ")=1 ",
+ selectionArgs);
+ } else {
+ String[] selectionArgs = new String[] {String.valueOf(optionalContactId),
+ String.valueOf(groupMembershipMimetypeId)};
+
+ db.execSQL("""
+ DELETE FROM visible_contacts
+ WHERE _id IN
+ (SELECT contacts._id
+ FROM contacts
+ WHERE contacts._id = ?
+ AND (""" + contactIsVisible + ")=0) ",
+ selectionArgs);
+
+ db.execSQL("""
+ INSERT INTO visible_contacts
+ SELECT _id
+ FROM contacts
+ WHERE _id = ? AND
+ _id NOT IN visible_contacts
+ AND (""" + contactIsVisible + ")=1 ",
+ selectionArgs);
+ }
}
/**
@@ -4722,6 +4775,586 @@
return sb.toString();
}
+ private interface Move {
+ String RAW_CONTACTS_ID_SELECT_FRAGMENT = (
+ "SELECT "
+ + RawContacts._ID + ", " + RawContacts.DISPLAY_NAME_PRIMARY
+ + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?"
+ + " AND " + RawContacts.DELETED + " = 0");
+
+ String DEDUPLICATION_QUERY = "SELECT "
+ + "source." + RawContacts._ID + " AS source_raw_contact_id,"
+ + " dest." + RawContacts._ID + " AS dest_raw_contact_id"
+ + " FROM (" + RAW_CONTACTS_ID_SELECT_FRAGMENT + ") source"
+ + " LEFT OUTER JOIN (" + RAW_CONTACTS_ID_SELECT_FRAGMENT + ") dest" + " ON "
+ + "source." + RawContacts.DISPLAY_NAME_PRIMARY + " = "
+ + "dest." + RawContacts.DISPLAY_NAME_PRIMARY;
+
+ String IS_NONSYSTEM_GROUP_FILTER = "("
+ + Groups.SYSTEM_ID + " IS NULL"
+ + " AND " + Groups.GROUP_IS_READ_ONLY + " = 0"
+ + ")";
+
+ String IS_SYSTEM_GROUP_FILTER = "("
+ + Groups.SYSTEM_ID + " IS NOT NULL"
+ + " OR " + Groups.GROUP_IS_READ_ONLY + " != 0"
+ + ")";
+
+ String GROUPS_ID_SELECT_FRAGMENT = (
+ "SELECT "
+ + Groups._ID + ", "
+ + Groups.TITLE
+ + " FROM " + Tables.GROUPS
+ + " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?"
+ + " AND " + Groups.DELETED + " = 0");
+ }
+
+ /**
+ * Count the number of {@link RawContacts} associated with the specified accounts.
+ * @param accounts the set of {@link AccountWithDataSet} to consider.
+ * @return the number of {@link RawContacts}.
+ */
+ public int countRawContactsQuery(Set<AccountWithDataSet> accounts) {
+ Set<Long> accountIds = accounts.stream()
+ .map(this::getAccountIdOrNull)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ try (Cursor c = getReadableDatabase().rawQuery("SELECT"
+ + " count(*) FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContactsColumns.ACCOUNT_ID + " IN "
+ + " (" + TextUtils.join(",", accountIds) + ")"
+ + " AND " + RawContacts.DELETED + " = 0",
+ new String[] {}
+ )) {
+ c.moveToFirst();
+ return c.getInt(0);
+ }
+ }
+
+ private Cursor getGroupDeduplicationQuery(
+ long sourceAccountId, long destAccountId, boolean isSystemGroupQuery) {
+ return getReadableDatabase().rawQuery(
+ "SELECT "
+ + "source." + Groups._ID + " AS source_group_id,"
+ + "dest." + Groups._ID + " AS dest_group_id"
+ + " FROM (" + Move.GROUPS_ID_SELECT_FRAGMENT + " AND "
+ + (isSystemGroupQuery
+ ? Move.IS_SYSTEM_GROUP_FILTER
+ : Move.IS_NONSYSTEM_GROUP_FILTER) + ") source"
+ + " LEFT OUTER JOIN (" + Move.GROUPS_ID_SELECT_FRAGMENT + ") dest ON "
+ + "source." + Groups.TITLE + " = "
+ + "dest." + Groups.TITLE,
+ new String[] {String.valueOf(sourceAccountId), String.valueOf(destAccountId)}
+ );
+ }
+
+ private Cursor getFirstPassDeduplicationQuery(
+ long sourceAccountId, long destAccountId) {
+ return getReadableDatabase().rawQuery(
+ Move.DEDUPLICATION_QUERY, new String[]{
+ String.valueOf(sourceAccountId), String.valueOf(destAccountId)}
+ );
+ }
+
+ private Cursor getSecondPassDeduplicationQuery(Set<Long> rawContactIds) {
+ return getReadableDatabase().rawQuery("SELECT "
+ + RawContactsColumns.CONCRETE_ID + ", "
+ + RawContactsColumns.CONCRETE_STARRED + ", "
+ + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
+ + Tables.DATA + "." + Data.IS_SUPER_PRIMARY + ", "
+ + DataColumns.CONCRETE_IS_PRIMARY + ", "
+ + DataColumns.MIMETYPE_ID + ", "
+ + DataColumns.CONCRETE_DATA1 + ", "
+ + DataColumns.CONCRETE_DATA2 + ", "
+ + DataColumns.CONCRETE_DATA3 + ", "
+ + DataColumns.CONCRETE_DATA4 + ", "
+ + DataColumns.CONCRETE_DATA5 + ", "
+ + DataColumns.CONCRETE_DATA6 + ", "
+ + DataColumns.CONCRETE_DATA7 + ", "
+ + DataColumns.CONCRETE_DATA8 + ", "
+ + DataColumns.CONCRETE_DATA9 + ", "
+ + DataColumns.CONCRETE_DATA10 + ", "
+ + DataColumns.CONCRETE_DATA11 + ", "
+ + DataColumns.CONCRETE_DATA12 + ", "
+ + DataColumns.CONCRETE_DATA13 + ", "
+ + DataColumns.CONCRETE_DATA14 + ", "
+ + DataColumns.CONCRETE_DATA15
+ + " FROM " + Tables.RAW_CONTACTS
+ + " LEFT OUTER JOIN " + Tables.DATA
+ + " ON " + RawContactsColumns.CONCRETE_ID + " = "
+ + DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " WHERE " + RawContactsColumns.CONCRETE_ID
+ + " IN (" + TextUtils.join(",", rawContactIds) + ")",
+ new String[]{});
+ }
+
+ /**
+ * Update GroupMembership DataRows from oldGroup to newGroup.
+ * @param oldGroup the old group.
+ * @param newGroup the new group.
+ */
+ public void updateGroupMemberships(Long oldGroup, Long newGroup) {
+ Long groupMembershipMimeType =
+ mCommonMimeTypeIdsCache.get(GroupMembership.CONTENT_ITEM_TYPE);
+ if (groupMembershipMimeType == null) {
+ // if we don't have a mimetype ID for group membership we know we don't have anything
+ // to update.
+ return;
+ }
+
+ try (SQLiteStatement updateGroupMembershipQuery = getWritableDatabase().compileStatement(
+ "UPDATE " + Tables.DATA
+ + " SET " + GroupMembership.GROUP_ROW_ID + "= ?"
+ + " WHERE "
+ + GroupMembership.GROUP_ROW_ID + "= ?"
+ + " AND " + DataColumns.MIMETYPE_ID + " = ?")) {
+
+ updateGroupMembershipQuery.bindLong(1, newGroup);
+ updateGroupMembershipQuery.bindLong(2, oldGroup);
+ updateGroupMembershipQuery.bindLong(3, groupMembershipMimeType);
+ updateGroupMembershipQuery.execute();
+ }
+ }
+
+ /**
+ * Compares the Groups in source and dest accounts, dividing the Groups in the
+ * source account into two sets - those which are duplicated in the destination account and
+ * those which are not.
+ *
+ * @param sourceAccount the source account
+ * @param destAccount the destination account
+ * @param isSystemGroupQuery true if we should deduplicate system groups, false if we should
+ * deduplicate non-system groups
+ * @return Pair of nonDuplicate ID set and the ID mapping (source to desk) for duplicates.
+ */
+ public Pair<Set<Long>, Map<Long, Long>> deDuplicateGroups(
+ AccountWithDataSet sourceAccount, AccountWithDataSet destAccount,
+ boolean isSystemGroupQuery) {
+ /*
+ First get the account ids
+ */
+ final Long sourceAccountId = getAccountIdOrNull(sourceAccount);
+ final Long destAccountId = getAccountIdOrNull(destAccount);
+ // if source account id is null then source account is empty, we are done
+ if (sourceAccountId == null) {
+
+ return Pair.create(Set.of(), Map.of());
+ }
+
+ // if dest account id is null, then dest account is empty, we can be sure everything in
+ // source is unique
+ Set<Long> nonDuplicates = new HashSet<>();
+ if (destAccountId == null) {
+ try (Cursor c = getReadableDatabase().query(
+ Tables.GROUPS,
+ new String[] {
+ Groups._ID,
+ },
+ GroupsColumns.ACCOUNT_ID + " = ?"
+ + " AND " + Groups.DELETED + " = 0"
+ + " AND " + (isSystemGroupQuery ? Move.IS_SYSTEM_GROUP_FILTER
+ : Move.IS_NONSYSTEM_GROUP_FILTER),
+ new String[] {sourceAccountId.toString()},
+ null, null, null)) {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(0);
+ nonDuplicates.add(rawContactId);
+ }
+ }
+ return Pair.create(nonDuplicates, Map.of());
+ }
+
+ HashMap<Long, Long> duplicates = new HashMap<>();
+ try (Cursor c = getGroupDeduplicationQuery(sourceAccountId, destAccountId,
+ isSystemGroupQuery)) {
+ while (c.moveToNext()) {
+ long sourceGroupId = c.getLong(0);
+ if (!c.isNull(1)) {
+ // if name matches, it's a duplicate
+ long destGroupId = c.getLong(1);
+ duplicates.put(sourceGroupId, destGroupId);
+ } else {
+ // add non name matching unique raw contacts to results.
+ nonDuplicates.add(sourceGroupId);
+ }
+ }
+ }
+
+ return Pair.create(nonDuplicates, duplicates);
+ }
+
+
+
+
+ /**
+ * Compares the raw contacts in source and dest accounts, dividing the raw contacts in the
+ * source account into two sets - those which are duplicated in the destination account and
+ * those which are not.
+ *
+ * @param sourceAccount the source account
+ * @param destAccount the destination account
+ * @return Pair of nonDuplicate ID set and the duplicate ID sets
+ */
+ public Pair<Set<Long>, Set<Long>> deDuplicateRawContacts(
+ AccountWithDataSet sourceAccount,
+ AccountWithDataSet destAccount) {
+ /*
+ First get the account ids
+ */
+ final Long sourceAccountId = getAccountIdOrNull(sourceAccount);
+ final Long destAccountId = getAccountIdOrNull(destAccount);
+ // if source account id is null then source account is empty, we are done
+ if (sourceAccountId == null) {
+
+ return Pair.create(Set.of(), Set.of());
+ }
+
+ // if dest account id is null, it is empty, everything in source is a non-duplicate
+ Set<Long> nonDuplicates = new HashSet<>();
+ if (destAccountId == null) {
+ try (Cursor c = getReadableDatabase().query(
+ Tables.RAW_CONTACTS,
+ new String[] {
+ RawContacts._ID,
+ },
+ RawContactsColumns.ACCOUNT_ID + " = ?"
+ + " AND " + RawContacts.DELETED + " = 0",
+ new String[] {sourceAccountId.toString()},
+ null, null, null)) {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(0);
+ nonDuplicates.add(rawContactId);
+ }
+ }
+ return Pair.create(nonDuplicates, Set.of());
+ }
+
+ /*
+ First discover potential duplicate by comparing names, which should filter out most of the
+ non-duplicate cases.
+ */
+ Set<Long> potentialDupSourceRawContactIds = new ArraySet<>();
+ Set<Long> potentialDupIds = new ArraySet<>();
+
+ try (Cursor c = getFirstPassDeduplicationQuery(sourceAccountId, destAccountId)) {
+ while (c.moveToNext()) {
+ long sourceRawContactIdId = c.getLong(0);
+ if (!c.isNull(1)) {
+ // if name matches, consider it a potential duplicate
+ long destRawContactId = c.getLong(1);
+ potentialDupSourceRawContactIds.add(sourceRawContactIdId);
+ potentialDupIds.add(sourceRawContactIdId);
+ potentialDupIds.add(destRawContactId);
+ } else {
+ // add non name matching unique raw contacts to results.
+ nonDuplicates.add(sourceRawContactIdId);
+ }
+ }
+ }
+
+ // if there are no potential duplicates at this point, then we are done
+ if (potentialDupIds.isEmpty()) {
+ return Pair.create(nonDuplicates, Set.of());
+ }
+
+ /*
+ Next, hash the potential duplicates.
+ */
+ Map<Long, Set<String>> sourceRawContactIdToHashSet = new HashMap<>();
+ Map<String, Set<Long>> destEntityHashes = new HashMap<>();
+ try (Cursor c = getSecondPassDeduplicationQuery(potentialDupIds)) {
+ while (c.moveToNext()) {
+ long id = c.getLong(0);
+ String hash = hashRawContactEntities(c);
+ if (potentialDupSourceRawContactIds.contains(id)) {
+ // if it's a source id, we'll want to build a set of hashes that represent it
+ if (!sourceRawContactIdToHashSet.containsKey(id)) {
+ Set<String> sourceHashes = new ArraySet<>();
+ sourceRawContactIdToHashSet.put(id, sourceHashes);
+ }
+ sourceRawContactIdToHashSet.get(id).add(hash);
+ } else {
+ // if it's a destination id, build a set of ids that it maps to
+ if (!destEntityHashes.containsKey(hash)) {
+ Set<Long> destIds = new ArraySet<>();
+ destEntityHashes.put(hash, destIds);
+ }
+ destEntityHashes.get(hash).add(id);
+ }
+ }
+ }
+
+ /*
+ Now use the hashes to determine which of the raw contact ids on the source account have
+ exact duplicates in the destination set.
+ */
+ Set<Long> duplicates = new ArraySet<>();
+ // At last, compare the raw entity hash to locate the exact duplicates
+ for (Map.Entry<Long, Set<String>> entry : sourceRawContactIdToHashSet.entrySet()) {
+ Long sourceRawContactId = entry.getKey();
+ Set<String> sourceHashes = entry.getValue();
+ if (hasDuplicateAtDestination(sourceHashes, destEntityHashes)) {
+ // if the source already has an exact match in the dest set, then it's a duplicate
+ duplicates.add(sourceRawContactId);
+ } else {
+ // if there is unique data on the source raw contact, add it to the unique set
+ nonDuplicates.add(sourceRawContactId);
+ }
+ }
+
+ return Pair.create(nonDuplicates, duplicates);
+ }
+
+ private boolean hasDuplicateAtDestination(Set<String> sourceHashes,
+ Map<String, Set<Long>> destHashToIdMap) {
+ // if we have no source hashes then treat it as unique
+ if (sourceHashes == null || sourceHashes.isEmpty()) {
+ // we should always have source hashes at this point so log something
+ Log.e(TAG, "empty source hashes while checking for duplicates during move");
+ return false;
+ }
+
+ Set<Long> potentialDestinationIds = null;
+ for (String sourceHash : sourceHashes) {
+ // if the source hash doesn't have a match in the dest account, we are done
+ if (!destHashToIdMap.containsKey(sourceHash)) {
+ return false;
+ }
+
+ // for all the matches in the destination account, intersect the sets of ids
+ if (potentialDestinationIds == null) {
+ potentialDestinationIds = new ArraySet<>(destHashToIdMap.get(sourceHash));
+ } else {
+ potentialDestinationIds.retainAll(destHashToIdMap.get(sourceHash));
+ }
+
+ // if the set of potential destination ids is ever empty, then we are done (no dupe)
+ if (potentialDestinationIds.isEmpty()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ private String hashRawContactEntities(final Cursor c) {
+ byte[] hashResult;
+ synchronized (mMessageDigest) {
+ mMessageDigest.reset();
+ for (int i = 1; i < c.getColumnCount(); i++) {
+ String data = c.getString(i);
+ if (!TextUtils.isEmpty(data)) {
+ mMessageDigest.update(data.getBytes());
+ }
+ }
+ hashResult = mMessageDigest.digest();
+ }
+
+ return Base64.encodeToString(hashResult, Base64.DEFAULT);
+ }
+
+ /**
+ * Delete all Data rows where MIMETYPE is not in ContactsContract.CommonDataKinds.
+ * @param account the account to delete data rows from.
+ */
+ public void deleteNonCommonDataRows(AccountWithDataSet account) {
+ final Long accountId = getAccountIdOrNull(account);
+ if (accountId == null) {
+ return;
+ }
+
+ try (SQLiteStatement nonPortableDataDelete = getWritableDatabase().compileStatement(
+ "DELETE FROM " + Tables.DATA
+ + " WHERE " + DataColumns.MIMETYPE_ID + " NOT IN ("
+ + TextUtils.join(",", mCommonMimeTypeIdsCache.values()) + ")"
+ + " AND " + Data.RAW_CONTACT_ID + " IN ("
+ + "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ? )"
+ )) {
+ nonPortableDataDelete.bindLong(1, accountId);
+ nonPortableDataDelete.execute();
+ }
+ }
+
+ private Set<Long> filterEmptyGroups(Set<Long> groupIds) {
+ if (groupIds == null || groupIds.isEmpty()) {
+ return Set.of();
+ }
+ Set<Long> nonEmptyGroupIds = new HashSet<>();
+ try (Cursor c = getReadableDatabase().query(
+ /* distinct= */ true,
+ Tables.DATA,
+ new String[] {
+ GroupMembership.GROUP_ROW_ID,
+ },
+ GroupMembership.GROUP_ROW_ID + " IN (" + TextUtils.join(",", groupIds) + ")"
+ + " AND " + DataColumns.MIMETYPE_ID + " = "
+ + mCommonMimeTypeIdsCache.get(GroupMembership.CONTENT_ITEM_TYPE),
+ new String[] {},
+ null, null, null, null)) {
+ while (c.moveToNext()) {
+ nonEmptyGroupIds.add(c.getLong(0));
+ }
+ }
+ return nonEmptyGroupIds;
+ }
+
+ /**
+ * Gets the content values for Groups that will be copied over during a move. Specifically, we
+ * move {@link ContactsContract.Groups#TITLE}, {@link ContactsContract.Groups#NOTES},
+ * {@link ContactsContract.Groups#GROUP_VISIBLE} {@link ContactsContract.Groups#TITLE_RES}, and
+ * {@link ContactsContract.Groups#RES_PACKAGE}.
+ * It will only create ContentValues for Groups that contain Contacts and will skip any with
+ * {@link ContactsContract.Groups#AUTO_ADD} set (as they are likely to simply be all contacts).
+ */
+ public Map<Long, ContentValues> getGroupContentValuesForMoveCopy(AccountWithDataSet account,
+ Set<Long> groupIds) {
+ // we only want move copies for non-empty groups
+ Set<Long> nonEmptyGroupIds = filterEmptyGroups(groupIds);
+ if (nonEmptyGroupIds == null || nonEmptyGroupIds.isEmpty()) {
+ return Map.of();
+ }
+ Map<Long, ContentValues> idToContentValues = new HashMap<>();
+ try (Cursor c = getReadableDatabase().query(
+ Tables.GROUPS,
+ new String[] {
+ Groups._ID,
+ Groups.GROUP_VISIBLE,
+ Groups.NOTES,
+ GroupsColumns.CONCRETE_PACKAGE_ID,
+ Groups.TITLE,
+ Groups.TITLE_RES,
+ },
+ Groups._ID + " IN (" + TextUtils.join(",", nonEmptyGroupIds) + ")"
+ + " AND " + Groups.AUTO_ADD + " = 0",
+ new String[] {},
+ null, null, null)) {
+ while (c.moveToNext()) {
+ Long originalGroupId = c.getLong(0);
+ ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(c, values);
+ // clear the existing ID from the content values
+ values.putNull(Groups._ID);
+ values.put(Groups.ACCOUNT_NAME, account.getAccountName());
+ values.put(Groups.ACCOUNT_TYPE, account.getAccountType());
+ values.put(Groups.DATA_SET, account.getDataSet());
+ idToContentValues.put(originalGroupId, values);
+ }
+ }
+
+ return idToContentValues;
+ }
+
+ /**
+ * Inserts sync stubs in account, for the groups in groupIds.
+ * The stubs consist of just the {@link ContactsContract.Groups#SOURCE_ID},
+ * {@link ContactsContract.Groups#SYNC1}, {@link ContactsContract.Groups#SYNC2},
+ * {@link ContactsContract.Groups#SYNC3}, and {@link ContactsContract.Groups#SYNC4} columns,
+ * with {@link ContactsContract.Groups#DELETED} and {@link ContactsContract.Groups#DIRTY} = 1.
+ * @param account the account to create the sync stubs in.
+ * @param groupIds the group ids to create sync stubs for.
+ */
+ public void insertGroupSyncStubs(AccountWithDataSet account,
+ Set<Long> groupIds) {
+ if (groupIds == null || groupIds.isEmpty()) {
+ return;
+ }
+ final long accountId = getOrCreateAccountIdInTransaction(account);
+
+ try (SQLiteStatement insertStubs = getWritableDatabase().compileStatement(
+ "INSERT INTO " + Tables.GROUPS
+ + "("
+ + Groups.SOURCE_ID + ","
+ + Groups.SYNC1 + ","
+ + Groups.SYNC2 + ","
+ + Groups.SYNC3 + ","
+ + Groups.SYNC4 + ","
+ + Groups.DELETED + ","
+ + Groups.DIRTY + ","
+ + GroupsColumns.ACCOUNT_ID
+ + ")"
+ + " SELECT "
+ + Groups.SOURCE_ID + ","
+ + Groups.SYNC1 + ","
+ + Groups.SYNC2 + ","
+ + Groups.SYNC3 + ","
+ + Groups.SYNC4 + ","
+ /* Groups.DELETED */ + "?,"
+ /* Groups.DIRTY */ + "?,"
+ /* GroupsColumns.ACCOUNT_ID */ + "?"
+ + " FROM " + Tables.GROUPS
+ + " WHERE "
+ + Groups._ID + " IN (" + TextUtils.join(",", groupIds) + ")"
+ + " AND " + Groups.SOURCE_ID + " IS NOT NULL"
+ )) {
+ // Groups.DELETED
+ insertStubs.bindLong(1, 1);
+ // Groups.DIRTY
+ insertStubs.bindLong(2, 1);
+ // GroupsColumns.ACCOUNT_ID
+ insertStubs.bindLong(3, accountId);
+ insertStubs.execute();
+ }
+ }
+
+ /**
+ * Inserts sync stubs in account, for the raw contacts in rawContactIds.
+ * The stubs consist of just the {@link ContactsContract.RawContacts#CONTACT_ID},
+ * {@link ContactsContract.RawContacts#SOURCE_ID}, {@link ContactsContract.RawContacts#SYNC1},
+ * {@link ContactsContract.RawContacts#SYNC2}, {@link ContactsContract.RawContacts#SYNC3}, and
+ * {@link ContactsContract.RawContacts#SYNC4} columns, with
+ * {@link ContactsContract.RawContacts#DELETED} and
+ * {@link ContactsContract.RawContacts#DIRTY} = 1.
+ * @param account the account to create the sync stubs in.
+ * @param rawContactIds the raw contact ids to create sync stubs for.
+ */
+ public void insertRawContactSyncStubs(AccountWithDataSet account,
+ Set<Long> rawContactIds) {
+ if (rawContactIds == null || rawContactIds.isEmpty()) {
+ return;
+ }
+ final long accountId = getOrCreateAccountIdInTransaction(account);
+
+ try (SQLiteStatement insertStubs = getWritableDatabase().compileStatement(
+ "INSERT INTO " + Tables.RAW_CONTACTS
+ + "("
+ + RawContacts.CONTACT_ID + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.SYNC1 + ","
+ + RawContacts.SYNC2 + ","
+ + RawContacts.SYNC3 + ","
+ + RawContacts.SYNC4 + ","
+ + RawContacts.DELETED + ","
+ + RawContacts.DIRTY + ","
+ + RawContactsColumns.ACCOUNT_ID
+ + ")"
+ + " SELECT "
+ + RawContacts.CONTACT_ID + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.SYNC1 + ","
+ + RawContacts.SYNC2 + ","
+ + RawContacts.SYNC3 + ","
+ + RawContacts.SYNC4 + ","
+ /* RawContacts.DELETED */ + "?,"
+ /* RawContacts.DIRTY */ + "?,"
+ /* RawContactsColumns.ACCOUNT_ID */ + "?"
+ + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE "
+ + RawContacts._ID + " IN (" + TextUtils.join(",", rawContactIds) + ")"
+ + " AND " + RawContacts.SOURCE_ID + " IS NOT NULL"
+ )) {
+ // RawContacts.DELETED
+ insertStubs.bindLong(1, 1);
+ // RawContacts.DIRTY
+ insertStubs.bindLong(2, 1);
+ // RawContactsColumns.ACCOUNT_ID
+ insertStubs.bindLong(3, accountId);
+ insertStubs.execute();
+ }
+ }
+
public void deleteStatusUpdate(long dataId) {
final SQLiteStatement statusUpdateDelete = getWritableDatabase().compileStatement(
"DELETE FROM " + Tables.STATUS_UPDATES +
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 8b10573..f15ade6 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -20,6 +20,8 @@
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static com.android.providers.contacts.flags.Flags.cp2SyncSearchIndexFlag;
+import static com.android.providers.contacts.flags.Flags.enableNewDefaultAccountRuleFlag;
import static com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME;
import android.accounts.Account;
@@ -1488,8 +1490,6 @@
private boolean mIsPhoneInitialized;
private boolean mIsPhone;
- private Account mAccount;
-
private AbstractContactAggregator mContactAggregator;
private AbstractContactAggregator mProfileAggregator;
@@ -1500,6 +1500,9 @@
private GlobalSearchSupport mGlobalSearchSupport;
private SearchIndexManager mSearchIndexManager;
+ private DefaultAccountManager mDefaultAccountManager;
+ private AccountResolver mAccountResolver;
+
private int mProviderStatus = STATUS_NORMAL;
private boolean mProviderStatusUpdateNeeded;
private volatile CountDownLatch mReadAccessLatch;
@@ -1623,6 +1626,11 @@
mContactDirectoryManager = new ContactDirectoryManager(this);
mGlobalSearchSupport = new GlobalSearchSupport(this);
+ mDefaultAccountManager = new DefaultAccountManager(getContext(), mContactsHelper);
+ mAccountResolver = new AccountResolver(mContactsHelper, mDefaultAccountManager);
+
+ mDefaultAccountManager = new DefaultAccountManager(getContext(), mContactsHelper);
+ mAccountResolver = new AccountResolver(mContactsHelper, mDefaultAccountManager);
if (mContactsHelper.getPhoneAccountHandleMigrationUtils()
.isPhoneAccountMigrationPending()) {
@@ -2576,8 +2584,13 @@
ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION);
final Bundle response = new Bundle();
- final Account defaultAccount = mDbHelper.get().getDefaultAccount();
- response.putParcelable(Settings.KEY_DEFAULT_ACCOUNT, defaultAccount);
+ final Account[] defaultAccount = mDbHelper.get().getDefaultAccountIfAny();
+
+ if (defaultAccount.length > 0) {
+ response.putParcelable(Settings.KEY_DEFAULT_ACCOUNT, defaultAccount[0]);
+ } else {
+ response.putParcelable(Settings.KEY_DEFAULT_ACCOUNT, null);
+ }
return response;
} else if (Settings.SET_DEFAULT_ACCOUNT_METHOD.equals(method)) {
@@ -2975,7 +2988,8 @@
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS: {
invalidateFastScrollingIndexCache();
- id = insertRawContact(uri, values, callerIsSyncAdapter);
+ id = insertRawContact(uri, values, callerIsSyncAdapter,
+ enableNewDefaultAccountRuleFlag() && match == RAW_CONTACTS);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -3006,7 +3020,8 @@
}
case GROUPS: {
- id = insertGroup(uri, values, callerIsSyncAdapter);
+ id = insertGroup(uri, values, callerIsSyncAdapter,
+ enableNewDefaultAccountRuleFlag());
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -3054,91 +3069,6 @@
return ContentUris.withAppendedId(uri, id);
}
- /**
- * If account is non-null then store it in the values. If the account is
- * already specified in the values then it must be consistent with the
- * account, if it is non-null.
- *
- * @param uri Current {@link Uri} being operated on.
- * @param values {@link ContentValues} to read and possibly update.
- * @throws IllegalArgumentException when only one of
- * {@link RawContacts#ACCOUNT_NAME} or
- * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
- * other undefined.
- * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
- * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
- * the given {@link Uri} and {@link ContentValues}.
- */
- private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
- String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
- String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
- final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
-
- String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
- String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
- final boolean partialValues = TextUtils.isEmpty(valueAccountName)
- ^ TextUtils.isEmpty(valueAccountType);
-
- if (partialUri || partialValues) {
- // Throw when either account is incomplete.
- throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
- "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
- }
-
- // Accounts are valid by only checking one parameter, since we've
- // already ruled out partial accounts.
- final boolean validUri = !TextUtils.isEmpty(accountName);
- final boolean validValues = !TextUtils.isEmpty(valueAccountName);
-
- if (validValues && validUri) {
- // Check that accounts match when both present
- final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
- && TextUtils.equals(accountType, valueAccountType);
- if (!accountMatch) {
- throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
- "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
- }
- } else if (validUri) {
- // Fill values from the URI when not present.
- values.put(RawContacts.ACCOUNT_NAME, accountName);
- values.put(RawContacts.ACCOUNT_TYPE, accountType);
- } else if (validValues) {
- accountName = valueAccountName;
- accountType = valueAccountType;
- } else {
- return null;
- }
-
- // Use cached Account object when matches, otherwise create
- if (mAccount == null
- || !mAccount.name.equals(accountName)
- || !mAccount.type.equals(accountType)) {
- mAccount = new Account(accountName, accountType);
- }
-
- return mAccount;
- }
-
- /**
- * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
- * in the URI or values (if any).
- * @param uri Current {@link Uri} being operated on.
- * @param values {@link ContentValues} to read and possibly update.
- */
- private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) {
- final Account account = resolveAccount(uri, values);
- AccountWithDataSet accountWithDataSet = null;
- if (account != null) {
- String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
- if (dataSet == null) {
- dataSet = values.getAsString(RawContacts.DATA_SET);
- } else {
- values.put(RawContacts.DATA_SET, dataSet);
- }
- accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
- }
- return accountWithDataSet;
- }
/**
* Inserts an item in the contacts table
@@ -3160,7 +3090,8 @@
* @return the ID of the newly-created row.
*/
private long insertRawContact(
- Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
+ Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter,
+ boolean applyDefaultAccount) {
inputValues = fixUpUsageColumnsForEdit(inputValues);
@@ -3169,7 +3100,8 @@
values.putNull(RawContacts.CONTACT_ID);
// Populate the relevant values before inserting the new entry into the database.
- final long accountId = replaceAccountInfoByAccountId(uri, values);
+ final long accountId = replaceAccountInfoByAccountId(uri, values,
+ applyDefaultAccount);
if (flagIsSet(values, RawContacts.DELETED)) {
values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
}
@@ -3529,12 +3461,14 @@
* and false otherwise.
* @return the ID of the newly-created row.
*/
- private long insertGroup(Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
+ private long insertGroup(Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter,
+ boolean applyDefaultAccount) {
// Create a shallow copy.
final ContentValues values = new ContentValues(inputValues);
// Populate the relevant values before inserting the new entry into the database.
- final long accountId = replaceAccountInfoByAccountId(uri, values);
+ final long accountId = replaceAccountInfoByAccountId(uri, values,
+ applyDefaultAccount);
replacePackageNameByPackageId(values);
if (!callerIsSyncAdapter) {
values.put(Groups.DIRTY, 1);
@@ -3573,7 +3507,8 @@
}
private Uri insertSettings(Uri uri, ContentValues values) {
- final AccountWithDataSet account = resolveAccountWithDataSet(uri, values);
+ final AccountWithDataSet account = mAccountResolver.resolveAccountWithDataSet(uri, values,
+ /*applyDefaultAccount=*/false);
// 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
@@ -4312,7 +4247,8 @@
values.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
values.putNull(RawContacts.CONTACT_ID);
values.put(RawContacts.DIRTY, 1);
- return updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+ return updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+ /*applyDefaultAccount=*/false);
}
static int deleteDataUsage(SQLiteDatabase db) {
@@ -4446,7 +4382,8 @@
case PROFILE_RAW_CONTACTS: {
invalidateFastScrollingIndexCache();
selection = appendAccountIdToSelection(uri, selection);
- count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
+ count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter,
+ enableNewDefaultAccountRuleFlag() && match == RAW_CONTACTS);
break;
}
@@ -4457,11 +4394,11 @@
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
count = updateRawContacts(values, RawContacts._ID + "=?"
+ " AND(" + selection + ")", selectionArgs,
- callerIsSyncAdapter);
+ callerIsSyncAdapter, enableNewDefaultAccountRuleFlag());
} else {
mSelectionArgs1[0] = String.valueOf(rawContactId);
count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
- callerIsSyncAdapter);
+ callerIsSyncAdapter, enableNewDefaultAccountRuleFlag());
}
break;
}
@@ -4752,6 +4689,11 @@
? updatedDataSet : c.getString(GroupAccountQuery.DATA_SET);
if (isAccountChanging) {
+ if (enableNewDefaultAccountRuleFlag()) {
+ mAccountResolver.checkAccountIsWritable(updatedAccountName,
+ updatedAccountType);
+ }
+
final long accountId = dbHelper.getOrCreateAccountIdInTransaction(
AccountWithDataSet.get(accountName, accountType, dataSet));
updatedValues.put(GroupsColumns.ACCOUNT_ID, accountId);
@@ -4808,7 +4750,7 @@
}
private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
- boolean callerIsSyncAdapter) {
+ boolean callerIsSyncAdapter, boolean applyDefaultAccount) {
if (values.containsKey(RawContacts.CONTACT_ID)) {
throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
"in content values. Contact IDs are assigned automatically");
@@ -4827,7 +4769,8 @@
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
- updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+ updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+ applyDefaultAccount);
count++;
}
} finally {
@@ -4860,7 +4803,7 @@
}
private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
- boolean callerIsSyncAdapter) {
+ boolean callerIsSyncAdapter, boolean applyDefaultAccount) {
final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
mSelectionArgs1[0] = Long.toString(rawContactId);
@@ -4916,6 +4859,23 @@
isDataSetChanging
? values.getAsString(RawContacts.DATA_SET) : oldDataSet
);
+
+ // The checkAccountIsWritable has to be done at the level of attempting to update
+ // each raw contacts, rather than at the beginning of attempting all selected raw
+ // contacts:
+ // since not all of account field (name, type, data_set) are provided in the
+ // ContentValues @param, the destination account of each raw contact can be
+ // partially derived from the their existing account info, and thus can be
+ // different.
+ // Since the UpdateRawContacts (updating all selected raw contacts) are done in
+ // a single transaction, failing checkAccountIsWritable will fail the entire update
+ // operation, which is clean such that no partial updated will be committed to the
+ // DB.
+ if (applyDefaultAccount) {
+ mAccountResolver.checkAccountIsWritable(newAccountWithDataSet.getAccountName(),
+ newAccountWithDataSet.getAccountType());
+ }
+
accountId = dbHelper.getOrCreateAccountIdInTransaction(newAccountWithDataSet);
values.put(RawContactsColumns.ACCOUNT_ID, accountId);
@@ -5432,166 +5392,7 @@
accountsWithDataSetsToDelete.add(knownAccountWithDataSet);
}
- if (!accountsWithDataSetsToDelete.isEmpty()) {
- for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
- final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet);
-
- if (accountIdOrNull != null) {
- final String accountId = Long.toString(accountIdOrNull);
- final String[] accountIdParams =
- new String[] {accountId};
- db.execSQL(
- "DELETE FROM " + Tables.GROUPS +
- " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
- accountIdParams);
- db.execSQL(
- "DELETE FROM " + Tables.PRESENCE +
- " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
- accountIdParams);
- db.execSQL(
- "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
- " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
- "SELECT " + StreamItems._ID +
- " FROM " + Tables.STREAM_ITEMS +
- " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))",
- accountIdParams);
- db.execSQL(
- "DELETE FROM " + Tables.STREAM_ITEMS +
- " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
- accountIdParams);
-
- // Delta API is only needed for regular contacts.
- if (!inProfileMode()) {
- // Contacts are deleted by a trigger on the raw_contacts table.
- // But we also need to insert the contact into the delete log.
- // This logic is being consolidated into the ContactsTableUtil.
-
- // deleteContactIfSingleton() does not work in this case because raw
- // contacts will be deleted in a single batch below. Contacts with
- // multiple raw contacts in the same account will be missed.
-
- // Find all contacts that do not have raw contacts in other accounts.
- // These should be deleted.
- Cursor cursor = db.rawQuery(
- "SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
- " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " IS NOT NULL" +
- " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " NOT IN (" +
- " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
- + " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " IS NOT NULL"
- + ")", accountIdParams);
- try {
- while (cursor.moveToNext()) {
- final long contactId = cursor.getLong(0);
- ContactsTableUtil.deleteContact(db, contactId);
- }
- } finally {
- MoreCloseables.closeQuietly(cursor);
- }
-
- // If the contact was not deleted, its last updated timestamp needs to
- // be refreshed since one of its raw contacts got removed.
- // Find all contacts that will not be deleted (i.e. contacts with
- // raw contacts in other accounts)
- cursor = db.rawQuery(
- "SELECT DISTINCT " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
- " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " IN (" +
- " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
- + ")", accountIdParams);
- try {
- while (cursor.moveToNext()) {
- final long contactId = cursor.getLong(0);
- ContactsTableUtil.updateContactLastUpdateByContactId(
- db, contactId);
- }
- } finally {
- MoreCloseables.closeQuietly(cursor);
- }
- }
-
- db.execSQL(
- "DELETE FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
- accountIdParams);
- db.execSQL(
- "DELETE FROM " + Tables.ACCOUNTS +
- " WHERE " + AccountsColumns._ID + "=?",
- accountIdParams);
- }
- }
-
- // Find all aggregated contacts that used to contain the raw contacts
- // we have just deleted and see if they are still referencing the deleted
- // names or photos. If so, fix up those contacts.
- ArraySet<Long> orphanContactIds = new ArraySet<>();
- Cursor cursor = db.rawQuery("SELECT " + Contacts._ID +
- " FROM " + Tables.CONTACTS +
- " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
- Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
- "(SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS + "))" +
- " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
- Contacts.PHOTO_ID + " NOT IN " +
- "(SELECT " + Data._ID +
- " FROM " + Tables.DATA + "))", null);
- try {
- while (cursor.moveToNext()) {
- orphanContactIds.add(cursor.getLong(0));
- }
- } finally {
- cursor.close();
- }
-
- for (Long contactId : orphanContactIds) {
- mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
- }
- dbHelper.updateAllVisible();
-
- // Don't bother updating the search index if we're in profile mode - there is no
- // search index for the profile DB, and updating it for the contacts DB in this case
- // makes no sense and risks a deadlock.
- if (!inProfileMode()) {
- // TODO Fix it. It only updates index for contacts/raw_contacts that the
- // current transaction context knows updated, but here in this method we don't
- // update that information, so effectively it's no-op.
- // We can probably just schedule BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
- // (But make sure it's not scheduled yet. We schedule this task in initialize()
- // too.)
- updateSearchIndexInTransaction();
- }
- }
-
- // Second, remove stale rows from Tables.DIRECTORIES
- removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME,
- Directory.ACCOUNT_TYPE, systemAccounts);
-
- // Third, remaining tasks that must be done in a transaction.
- // TODO: Should sync state take data set into consideration?
- dbHelper.getSyncState().onAccountsChanged(db, systemAccounts);
-
- saveAccounts(systemAccounts);
-
- db.setTransactionSuccessful();
+ removeDataOfAccount(systemAccounts, accountsWithDataSetsToDelete, dbHelper, db);
} finally {
db.endTransaction();
}
@@ -5602,6 +5403,205 @@
return true;
}
+ private void removeDataOfAccount(Account[] systemAccounts,
+ List<AccountWithDataSet> accountsWithDataSetsToDelete, ContactsDatabaseHelper dbHelper,
+ SQLiteDatabase db) {
+ if (!accountsWithDataSetsToDelete.isEmpty()) {
+ for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
+ final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet);
+
+ if (accountIdOrNull != null) {
+ final String accountId = Long.toString(accountIdOrNull);
+ final String[] accountIdParams =
+ new String[] {accountId};
+ db.execSQL(
+ "DELETE FROM " + Tables.GROUPS +
+ " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
+ accountIdParams);
+ db.execSQL(
+ "DELETE FROM " + Tables.PRESENCE +
+ " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
+ "SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
+ accountIdParams);
+ db.execSQL(
+ "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
+ " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
+ "SELECT " + StreamItems._ID +
+ " FROM " + Tables.STREAM_ITEMS +
+ " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
+ "SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))",
+ accountIdParams);
+ db.execSQL(
+ "DELETE FROM " + Tables.STREAM_ITEMS +
+ " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
+ "SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
+ accountIdParams);
+
+ // Delta API is only needed for regular contacts.
+ if (!inProfileMode()) {
+ // Contacts are deleted by a trigger on the raw_contacts table.
+ // But we also need to insert the contact into the delete log.
+ // This logic is being consolidated into the ContactsTableUtil.
+
+ // deleteContactIfSingleton() does not work in this case because raw
+ // contacts will be deleted in a single batch below. Contacts with
+ // multiple raw contacts in the same account will be missed.
+
+ // Find all contacts that do not have raw contacts in other accounts.
+ // These should be deleted.
+ Cursor cursor = db.rawQuery(
+ "SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
+ " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " IS NOT NULL" +
+ " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " NOT IN (" +
+ " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
+ + " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " IS NOT NULL"
+ + ")", accountIdParams);
+ try {
+ while (cursor.moveToNext()) {
+ final long contactId = cursor.getLong(0);
+ ContactsTableUtil.deleteContact(db, contactId);
+ if (cp2SyncSearchIndexFlag()) {
+ mTransactionContext.get()
+ .invalidateSearchIndexForContact(contactId);
+ }
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+
+ // If the contact was not deleted, its last updated timestamp needs to
+ // be refreshed since one of its raw contacts got removed.
+ // Find all contacts that will not be deleted (i.e. contacts with
+ // raw contacts in other accounts)
+ cursor = db.rawQuery(
+ "SELECT DISTINCT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
+ " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " IN (" +
+ " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
+ + ")", accountIdParams);
+ try {
+ while (cursor.moveToNext()) {
+ final long contactId = cursor.getLong(0);
+ ContactsTableUtil.updateContactLastUpdateByContactId(
+ db, contactId);
+ if (cp2SyncSearchIndexFlag()) {
+ mTransactionContext.get()
+ .invalidateSearchIndexForContact(contactId);
+ }
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ }
+
+ db.execSQL(
+ "DELETE FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
+ accountIdParams);
+ db.execSQL(
+ "DELETE FROM " + Tables.ACCOUNTS +
+ " WHERE " + AccountsColumns._ID + "=?",
+ accountIdParams);
+ }
+ }
+
+ // Find all aggregated contacts that used to contain the raw contacts
+ // we have just deleted and see if they are still referencing the deleted
+ // names or photos. If so, fix up those contacts.
+ ArraySet<Long> orphanContactIds = new ArraySet<>();
+ Cursor cursor = db.rawQuery("SELECT " + Contacts._ID +
+ " FROM " + Tables.CONTACTS +
+ " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
+ Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
+ "(SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS + "))" +
+ " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
+ Contacts.PHOTO_ID + " NOT IN " +
+ "(SELECT " + Data._ID +
+ " FROM " + Tables.DATA + "))", null);
+ try {
+ while (cursor.moveToNext()) {
+ orphanContactIds.add(cursor.getLong(0));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ for (Long contactId : orphanContactIds) {
+ mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
+ }
+ dbHelper.updateAllVisible();
+
+ // Don't bother updating the search index if we're in profile mode - there is no
+ // search index for the profile DB, and updating it for the contacts DB in this case
+ // makes no sense and risks a deadlock.
+ if (!inProfileMode()) {
+ // Will remove the deleted contact ids of the account from the search index and
+ // will update the contacts in the search index which had a raw contact deleted.
+ updateSearchIndexInTransaction();
+ }
+ }
+
+ // Second, remove stale rows from Tables.DIRECTORIES
+ removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME,
+ Directory.ACCOUNT_TYPE, systemAccounts);
+
+ // Third, remaining tasks that must be done in a transaction.
+ // TODO: Should sync state take data set into consideration?
+ dbHelper.getSyncState().onAccountsChanged(db, systemAccounts);
+
+ saveAccounts(systemAccounts);
+
+ db.setTransactionSuccessful();
+ }
+
+ @VisibleForTesting
+ void unSyncAccounts(Account[] accountsToUnSync) {
+ List<AccountWithDataSet> accountWithDataSetList =
+ mDbHelper.get().getAllAccountsWithDataSets().stream().filter(
+ accountWithDataSet -> accountWithDataSet.inSystemAccounts(
+ accountsToUnSync)).toList();
+ Account[] accounts = AccountManager.get(getContext()).getAccounts();
+ switchToContactMode();
+ final ContactsDatabaseHelper dbHelper = mDbHelper.get();
+ final SQLiteDatabase db = dbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ removeDataOfAccount(accounts, accountWithDataSetList, dbHelper,
+ dbHelper.getWritableDatabase());
+ } finally {
+ db.endTransaction();
+ }
+ switchToProfileMode();
+ db.beginTransaction();
+ try {
+ removeDataOfAccount(accounts, accountWithDataSetList, dbHelper,
+ dbHelper.getWritableDatabase());
+ } finally {
+ db.endTransaction();
+ }
+ switchToContactMode();
+ updateContactsAccountCount(accounts);
+ updateDirectoriesInBackground(true);
+ }
+
private void updateContactsAccountCount(Account[] accounts) {
int count = 0;
for (Account account : accounts) {
@@ -10290,8 +10290,10 @@
* @param values The {@link ContentValues} object to operate on.
* @return The corresponding account ID.
*/
- private long replaceAccountInfoByAccountId(Uri uri, ContentValues values) {
- final AccountWithDataSet account = resolveAccountWithDataSet(uri, values);
+ private long replaceAccountInfoByAccountId(Uri uri, ContentValues values,
+ boolean applyDefaultAccount) {
+ final AccountWithDataSet account = mAccountResolver.resolveAccountWithDataSet(uri, values,
+ applyDefaultAccount);
final long id = mDbHelper.get().getOrCreateAccountIdInTransaction(account);
values.put(RawContactsColumns.ACCOUNT_ID, id);
@@ -10450,4 +10452,10 @@
public ProfileProvider getProfileProviderForTest() {
return mProfileProvider;
}
+
+ /** Should be only used in tests. */
+ @NeededForTesting
+ void setSearchIndexMaxUpdateFilterContacts(int maxUpdateFilterContacts) {
+ mSearchIndexManager.setMaxUpdateFilterContacts(maxUpdateFilterContacts);
+ }
}
diff --git a/src/com/android/providers/contacts/DefaultAccount.java b/src/com/android/providers/contacts/DefaultAccount.java
new file mode 100644
index 0000000..a8b4164
--- /dev/null
+++ b/src/com/android/providers/contacts/DefaultAccount.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 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.accounts.Account;
+
+/**
+ * Represents a default account with a category (UNKNOWN, DEVICE, or CLOUD)
+ * and an optional associated Android Account object.
+ */
+public class DefaultAccount {
+ /**
+ * The possible categories for a DefaultAccount.
+ */
+ public enum AccountCategory {
+ /**
+ * The account category is unknown. This is usually a temporary state.
+ */
+ UNKNOWN,
+
+ /**
+ * The account is a device-only account and not synced to the cloud.
+ */
+ DEVICE,
+
+ /**
+ * The account is synced to the cloud.
+ */
+ CLOUD
+ }
+
+
+ public static final DefaultAccount UNKNOWN_DEFAULT_ACCOUNT = new DefaultAccount(
+ AccountCategory.UNKNOWN, null);
+ public static final DefaultAccount DEVICE_DEFAULT_ACCOUNT = new DefaultAccount(
+ AccountCategory.DEVICE, null);
+
+ /**
+ * Create a DefaultAccount object which points to the cloud.
+ * @param cloudAccount The cloud account that is being set as the default account.
+ * @return The DefaultAccount object.
+ */
+ public static DefaultAccount ofCloud(Account cloudAccount) {
+ return new DefaultAccount(AccountCategory.CLOUD, cloudAccount);
+ }
+
+ private final AccountCategory mAccountCategory;
+ private final Account mCloudAccount;
+
+ /**
+ * Constructs a DefaultAccount object.
+ *
+ * @param accountCategory The category of the default account.
+ * @param cloudAccount The account when mAccountCategory is CLOUD (null for
+ * DEVICE/UNKNOWN).
+ * @throws IllegalArgumentException If cloudAccount is null when accountCategory is
+ * CLOUD,
+ * or if cloudAccount is not null when accountCategory is not
+ * CLOUD.
+ */
+ public DefaultAccount(AccountCategory accountCategory, Account cloudAccount) {
+ this.mAccountCategory = accountCategory;
+
+ // Validate cloudAccount based on accountCategory
+ if (accountCategory == AccountCategory.CLOUD && cloudAccount == null) {
+ throw new IllegalArgumentException(
+ "Cloud account cannot be null when category is CLOUD");
+ } else if (accountCategory != AccountCategory.CLOUD && cloudAccount != null) {
+ throw new IllegalArgumentException(
+ "Cloud account should be null when category is not CLOUD");
+ }
+
+ this.mCloudAccount = cloudAccount;
+ }
+
+ /**
+ * Gets the category of the account.
+ *
+ * @return The current category (UNKNOWN, DEVICE, or CLOUD).
+ */
+ public AccountCategory getAccountCategory() {
+ return mAccountCategory;
+ }
+
+ /**
+ * Gets the associated cloud account, if available.
+ *
+ * @return The Android Account object, or null if the category is not CLOUD.
+ */
+ public Account getCloudAccount() {
+ return mCloudAccount;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true; // Same object
+ if (o == null || getClass() != o.getClass()) return false; // Null or different class
+
+ DefaultAccount that = (DefaultAccount) o;
+
+ // Compare account categories first for efficiency
+ if (mAccountCategory != that.mAccountCategory) return false;
+
+ // If categories match, compare cloud accounts depending on category
+ if (mAccountCategory == AccountCategory.CLOUD) {
+ return mCloudAccount.equals(that.mCloudAccount); // Use Account's equals
+ } else {
+ return true; // Categories match and cloud account is irrelevant
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mAccountCategory.hashCode();
+ if (mAccountCategory == AccountCategory.CLOUD) {
+ result = 31 * result + mCloudAccount.hashCode(); // Use Account's hashCode
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{mAccountCategory: %s, mCloudAccount: %s}",
+ mAccountCategory, mCloudAccount);
+ }
+
+}
diff --git a/src/com/android/providers/contacts/DefaultAccountManager.java b/src/com/android/providers/contacts/DefaultAccountManager.java
new file mode 100644
index 0000000..c42aac1
--- /dev/null
+++ b/src/com/android/providers/contacts/DefaultAccountManager.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.providers.contacts.util.NeededForTesting;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A utility class to provide methods to load and set the default account.
+ */
+@NeededForTesting
+public class DefaultAccountManager {
+ private static final String TAG = "DefaultAccountManager";
+
+ private static HashSet<String> sEligibleSystemCloudAccountTypes = null;
+
+ private final Context mContext;
+ private final ContactsDatabaseHelper mDbHelper;
+ private final SyncSettingsHelper mSyncSettingsHelper;
+ private final AccountManager mAccountManager;
+
+ DefaultAccountManager(Context context, ContactsDatabaseHelper dbHelper) {
+ this(context, dbHelper, new SyncSettingsHelper(), AccountManager.get(context));
+ }
+
+ // Keep it in proguard for testing: once it's used in production code, remove this annotation.
+ @NeededForTesting
+ DefaultAccountManager(Context context, ContactsDatabaseHelper dbHelper,
+ SyncSettingsHelper syncSettingsHelper, AccountManager accountManager) {
+ mContext = context;
+ mDbHelper = dbHelper;
+ mSyncSettingsHelper = syncSettingsHelper;
+ mAccountManager = accountManager;
+ }
+
+ private static synchronized Set<String> getEligibleSystemAccountTypes(Context context) {
+ if (sEligibleSystemCloudAccountTypes == null) {
+ sEligibleSystemCloudAccountTypes = new HashSet<>();
+
+ Resources resources = Resources.getSystem();
+ String[] accountTypesArray =
+ resources.getStringArray(R.array.config_rawContactsEligibleDefaultAccountTypes);
+
+ sEligibleSystemCloudAccountTypes.addAll(Arrays.asList(accountTypesArray));
+ }
+ return sEligibleSystemCloudAccountTypes;
+ }
+
+ @NeededForTesting
+ static synchronized void setEligibleSystemCloudAccountTypesForTesting(String[] accountTypes) {
+ sEligibleSystemCloudAccountTypes = new HashSet<>(Arrays.asList(accountTypes));
+ }
+
+ /**
+ * Try to push an account as the default account.
+ *
+ * @param defaultAccount account to be set as the default account.
+ * @return true if the default account is successfully updated.
+ */
+ @NeededForTesting
+ public boolean tryPushDefaultAccount(DefaultAccount defaultAccount) {
+ if (!isValidDefaultAccount(defaultAccount)) {
+ Log.w(TAG, "Attempt to push an invalid default account.");
+ return false;
+ }
+
+ DefaultAccount previousDefaultAccount = pullDefaultAccount();
+
+ if (defaultAccount.equals(previousDefaultAccount)) {
+ Log.w(TAG, "Account has already been set as default before");
+ return false;
+ }
+
+ directlySetDefaultAccountInDb(defaultAccount);
+ return true;
+ }
+
+ private boolean isValidDefaultAccount(DefaultAccount defaultAccount) {
+ if (defaultAccount.getAccountCategory() == DefaultAccount.AccountCategory.CLOUD) {
+ return defaultAccount.getCloudAccount() != null
+ && isSystemCloudAccount(defaultAccount.getCloudAccount())
+ && !mSyncSettingsHelper.isSyncOff(defaultAccount.getCloudAccount());
+ }
+ return defaultAccount.getCloudAccount() == null;
+ }
+
+ /**
+ * Pull the default account from the DB.
+ */
+ @NeededForTesting
+ public DefaultAccount pullDefaultAccount() {
+ DefaultAccount defaultAccount = getDefaultAccountFromDb();
+
+ if (isValidDefaultAccount(defaultAccount)) {
+ return defaultAccount;
+ } else {
+ Log.w(TAG, "Default account stored in the DB is no longer valid.");
+ directlySetDefaultAccountInDb(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ return DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT;
+ }
+ }
+
+ private void directlySetDefaultAccountInDb(DefaultAccount defaultAccount) {
+ switch (defaultAccount.getAccountCategory()) {
+ case UNKNOWN: {
+ mDbHelper.clearDefaultAccount();
+ break;
+ }
+ case DEVICE: {
+ mDbHelper.setDefaultAccount(AccountWithDataSet.LOCAL.getAccountName(),
+ AccountWithDataSet.LOCAL.getAccountType());
+ break;
+ }
+ case CLOUD:
+ mDbHelper.setDefaultAccount(defaultAccount.getCloudAccount().name,
+ defaultAccount.getCloudAccount().type);
+ break;
+ default:
+ Log.e(TAG, "Incorrect default account category");
+ break;
+ }
+ }
+
+ private boolean isSystemCloudAccount(Account account) {
+ if (account == null || !getEligibleSystemAccountTypes(mContext).contains(account.type)) {
+ return false;
+ }
+
+ Account[] accountsInThisType = mAccountManager.getAccountsByType(account.type);
+ for (Account currentAccount : accountsInThisType) {
+ if (currentAccount.equals(account)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private DefaultAccount getDefaultAccountFromDb() {
+ Account[] defaultAccountFromDb = mDbHelper.getDefaultAccountIfAny();
+ if (defaultAccountFromDb.length == 0) {
+ return DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT;
+ }
+
+ if (defaultAccountFromDb[0] == null) {
+ return DefaultAccount.DEVICE_DEFAULT_ACCOUNT;
+ }
+
+ if (defaultAccountFromDb[0].name.equals(AccountWithDataSet.LOCAL.getAccountName())
+ && defaultAccountFromDb[0].type.equals(AccountWithDataSet.LOCAL.getAccountType())) {
+ return DefaultAccount.DEVICE_DEFAULT_ACCOUNT;
+ }
+
+ return DefaultAccount.ofCloud(defaultAccountFromDb[0]);
+ }
+}
diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java
index aeaa0e7..38a91f1 100644
--- a/src/com/android/providers/contacts/SearchIndexManager.java
+++ b/src/com/android/providers/contacts/SearchIndexManager.java
@@ -15,6 +15,8 @@
*/
package com.android.providers.contacts;
+import static com.android.providers.contacts.flags.Flags.cp2SyncSearchIndexFlag;
+
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -52,6 +54,7 @@
private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+ public static final int MAX_UPDATE_FILTER_CONTACTS = 5000;
private static final int MAX_STRING_BUILDER_SIZE = 1024 * 10;
public static final String PROPERTY_SEARCH_INDEX_VERSION = "search_index";
@@ -246,6 +249,7 @@
private IndexBuilder mIndexBuilder = new IndexBuilder();
private ContentValues mValues = new ContentValues();
private String[] mSelectionArgs1 = new String[1];
+ private int mMaxUpdateFilterContacts = MAX_UPDATE_FILTER_CONTACTS;
public SearchIndexManager(ContactsProvider2 contactsProvider) {
this.mContactsProvider = contactsProvider;
@@ -296,44 +300,70 @@
Log.v(TAG, "Updating search index for " + contactIds.size() +
" contacts / " + rawContactIds.size() + " raw contacts");
}
+
+ final long contactsCount = contactIds.size() + rawContactIds.size();
+
StringBuilder sb = new StringBuilder();
- sb.append("(");
- if (!contactIds.isEmpty()) {
- // Select all raw contacts that belong to all contacts in contactIds
- sb.append(RawContacts.CONTACT_ID + " IN (");
- sb.append(TextUtils.join(",", contactIds));
- sb.append(')');
- }
- if (!rawContactIds.isEmpty()) {
+ if (!cp2SyncSearchIndexFlag() || contactsCount <= mMaxUpdateFilterContacts) {
+ sb.append("(");
if (!contactIds.isEmpty()) {
- sb.append(" OR ");
+ // Select all raw contacts that belong to all contacts in contactIds
+ sb.append(RawContacts.CONTACT_ID + " IN (");
+ sb.append(TextUtils.join(",", contactIds));
+ sb.append(')');
}
- // Select all raw contacts that belong to the same contact as all raw contacts
- // in rawContactIds. For every raw contact in rawContactIds that we are updating
- // the index for, we need to rebuild the search index for all raw contacts belonging
- // to the same contact, because we can only update the search index on a per-contact
- // basis.
- sb.append(RawContacts.CONTACT_ID + " IN " +
- "(SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.CONCRETE_ID + " IN (");
- sb.append(TextUtils.join(",", rawContactIds));
- sb.append("))");
+ if (!rawContactIds.isEmpty()) {
+ if (!contactIds.isEmpty()) {
+ sb.append(" OR ");
+ }
+ // Select all raw contacts that belong to the same contact as all raw contacts
+ // in rawContactIds. For every raw contact in rawContactIds that we are updating
+ // the index for, we need to rebuild the search index for all raw contacts belonging
+ // to the same contact, because we can only update the search index on a per-contact
+ // basis.
+ sb.append(RawContacts.CONTACT_ID + " IN "
+ + "(SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN (");
+ sb.append(TextUtils.join(",", rawContactIds));
+ sb.append("))");
+ }
+ sb.append(")");
}
- sb.append(")");
-
- // The selection to select raw_contacts.
- final String rawContactsSelection = sb.toString();
+ // The selection to select raw_contacts. If the selection string is empty
+ // the entire search index table will be rebuilt.
+ String rawContactsSelection = sb.toString();
// Remove affected search_index rows.
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- final int deleted = db.delete(Tables.SEARCH_INDEX,
- ROW_ID_KEY + " IN (SELECT " +
- RawContacts.CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + rawContactsSelection +
- ")"
- , null);
+ if (cp2SyncSearchIndexFlag()) {
+ // If the amount of contacts which need to be re-synced in the search index
+ // surpasses the limit, then simply clear the entire search index table and
+ // and rebuild it.
+ String whereClause = null;
+ if (contactsCount <= mMaxUpdateFilterContacts) {
+ // Only remove the provided contacts
+ whereClause =
+ "rowid IN ("
+ + TextUtils.join(",", contactIds)
+ + """
+ ) OR rowid IN (
+ SELECT contact_id
+ FROM raw_contacts
+ WHERE raw_contacts._id IN ("""
+ + TextUtils.join(",", rawContactIds)
+ + "))";
+ }
+ db.delete(Tables.SEARCH_INDEX, whereClause, null);
+ } else {
+ db.delete(Tables.SEARCH_INDEX,
+ ROW_ID_KEY + " IN (SELECT "
+ + RawContacts.CONTACT_ID
+ + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + rawContactsSelection
+ + ")",
+ null);
+ }
// Then rebuild index for them.
final int count = buildAndInsertIndex(db, rawContactsSelection);
@@ -404,6 +434,7 @@
mValues.put(ROW_ID_KEY, contactId);
db.insert(Tables.SEARCH_INDEX, null, mValues);
}
+
private int getSearchIndexVersion() {
return Integer.parseInt(mDbHelper.getProperty(PROPERTY_SEARCH_INDEX_VERSION, "0"));
}
@@ -412,6 +443,11 @@
mDbHelper.setProperty(PROPERTY_SEARCH_INDEX_VERSION, String.valueOf(version));
}
+ @VisibleForTesting
+ void setMaxUpdateFilterContacts(int maxUpdateFilterContacts) {
+ mMaxUpdateFilterContacts = maxUpdateFilterContacts;
+ }
+
/**
* Token separator that matches SQLite's "simple" tokenizer.
* - Unicode codepoints >= 128: Everything
diff --git a/src/com/android/providers/contacts/SyncSettingsHelper.java b/src/com/android/providers/contacts/SyncSettingsHelper.java
new file mode 100644
index 0000000..1e950c4
--- /dev/null
+++ b/src/com/android/providers/contacts/SyncSettingsHelper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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.accounts.Account;
+
+import com.android.providers.contacts.util.NeededForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@NeededForTesting
+public class SyncSettingsHelper {
+ @NeededForTesting
+ public enum SyncState { ON, OFF }
+
+ // TODO: Currently the sync state are stored in memory, which will be hooked up with the real
+ // sync settings.
+ private final Map<Account, SyncState> mSyncStates;
+
+ public SyncSettingsHelper() {
+ mSyncStates = new HashMap<>();
+ }
+
+ /**
+ * Turns on sync for the given account.
+ *
+ * @param account The account for which sync should be turned on.
+ */
+ @NeededForTesting
+ public void turnOnSync(Account account) {
+ mSyncStates.put(account, SyncState.ON);
+ }
+
+ /**
+ * Turns off sync for the given account.
+ *
+ * @param account The account for which sync should be turned off.
+ */
+ @NeededForTesting
+ public void turnOffSync(Account account) {
+ mSyncStates.put(account, SyncState.OFF);
+ }
+
+ /**
+ * Checks if sync is turned off for the given account.
+ *
+ * @param account The account to check.
+ * @return false if sync is off, true otherwise.
+ */
+ @NeededForTesting
+ public boolean isSyncOff(Account account) {
+ return mSyncStates.get(account) == SyncState.OFF;
+ }
+}
+
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
index cb649ca..0accfb0 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
@@ -16,9 +16,11 @@
package com.android.providers.contacts.aggregation;
+import static com.android.providers.contacts.flags.Flags.cp2SyncSearchIndexFlag;
import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_PRIMARY;
import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SECONDARY;
import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SUGGEST;
+
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.AggregationExceptions;
@@ -33,6 +35,7 @@
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+
import com.android.providers.contacts.ContactsDatabaseHelper;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
@@ -44,12 +47,12 @@
import com.android.providers.contacts.NameSplitter;
import com.android.providers.contacts.PhotoPriorityResolver;
import com.android.providers.contacts.TransactionContext;
-import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
import com.android.providers.contacts.aggregation.util.MatchScore;
import com.android.providers.contacts.aggregation.util.RawContactMatcher;
import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates;
import com.android.providers.contacts.database.ContactsTableUtil;
+
import com.google.android.collect.Sets;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
@@ -379,9 +382,16 @@
if (currentRcCount == 0) {
// Delete a contact if it doesn't contain anything
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Deleting contact id: " + cid);
+ }
ContactsTableUtil.deleteContact(db, cid);
mAggregatedPresenceDelete.bindLong(1, cid);
mAggregatedPresenceDelete.execute();
+ if (cp2SyncSearchIndexFlag()) {
+ // Make sure we remove the obsolete contact id from search index
+ txContext.invalidateSearchIndexForContact(cid);
+ }
} else {
updateAggregateData(txContext, cid);
}
diff --git a/tests/Android.bp b/tests/Android.bp
index de6d3f0..96a0cc1 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -12,6 +12,7 @@
"mockito-target-minus-junit4",
"flag-junit",
"android.content.pm.flags-aconfig-java",
+ "contactsprovider_flags_java_lib",
],
libs: [
"android.test.runner.stubs.system",
@@ -29,3 +30,42 @@
enabled: false,
},
}
+
+// Tests with all launch-able flags enabled by default.
+// All flags' value will be true unless overridden in the individual tests.
+test_module_config {
+ name: "ContactsProviderTestsWithAllFlagEnabled",
+ base: "ContactsProviderTests",
+ test_suites: ["device-tests"],
+
+ options: [
+ {
+ name: "feature-flags:flag-value",
+ value: "contacts/com.android.providers.contacts.flags.cp2_account_move_flag=true",
+ },
+ {
+ name: "feature-flags:flag-value",
+ value: "contacts/com.android.providers.contacts.flags.enable_new_default_account_rule_flag=true",
+ },
+
+ ],
+}
+
+// Tests with all launch-able flags disabled by default.
+// All flags' value will be false unless overridden in the individual tests.
+test_module_config {
+ name: "ContactsProviderTestsWithAllFlagDisabled",
+ base: "ContactsProviderTests",
+ test_suites: ["device-tests"],
+
+ options: [
+ {
+ name: "feature-flags:flag-value",
+ value: "contacts/com.android.providers.contacts.flags.cp2_account_move_flag=false",
+ },
+ {
+ name: "feature-flags:flag-value",
+ value: "contacts/com.android.providers.contacts.flags.enable_new_default_account_rule_flag=false",
+ },
+ ],
+}
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index 967614c..7c6e2b9 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -17,6 +17,7 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ContactsProviderTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" />
<option name="test-suite-tag" value="apct" />
<option name="test-tag" value="ContactsProviderTests" />
diff --git a/tests/src/com/android/providers/contacts/AccountResolverTest.java b/tests/src/com/android/providers/contacts/AccountResolverTest.java
new file mode 100644
index 0000000..c0f82f3
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/AccountResolverTest.java
@@ -0,0 +1,717 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
+import android.accounts.Account;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract.RawContacts;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.providers.contacts.DefaultAccount.AccountCategory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class AccountResolverTest {
+ @Mock
+ private ContactsDatabaseHelper mDbHelper;
+ @Mock
+ private DefaultAccountManager mDefaultAccountManager;
+
+ private AccountResolver mAccountResolver;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAccountResolver = new AccountResolver(mDbHelper, mDefaultAccountManager);
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_accountAndDataSetInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .appendQueryParameter(RawContacts.DATA_SET, "test_data_set")
+ .build();
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result.getAccountName());
+ assertEquals("com.google", result.getAccountType());
+ assertEquals("test_data_set", result.getDataSet());
+ assertEquals("test_data_set", values.getAsString(RawContacts.DATA_SET));
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsUnknown_accountAndDataSetInUri() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .appendQueryParameter(RawContacts.DATA_SET, "test_data_set")
+ .build();
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+
+ assertEquals("test_account", result.getAccountName());
+ assertEquals("com.google", result.getAccountType());
+ assertEquals("test_data_set", result.getDataSet());
+ assertEquals("test_data_set", values.getAsString(RawContacts.DATA_SET));
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_accountInUriDataSetInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result.getAccountName());
+ assertEquals("com.google", result.getAccountType());
+ assertEquals("test_data_set", result.getDataSet());
+ assertEquals("test_data_set", values.getAsString(RawContacts.DATA_SET));
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_applyDefaultAccount_accountInUriDataSetInValues() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(new DefaultAccount(
+ AccountCategory.CLOUD, new Account("randomaccount1@gmail.com", "com.google")));
+
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result.getAccountName());
+ assertEquals("com.google", result.getAccountType());
+ assertEquals("test_data_set", result.getDataSet());
+ assertEquals("test_data_set", values.getAsString(RawContacts.DATA_SET));
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_noAccount() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsUnknown_noAccount() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsDevice_noAccount() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsCloud_noAccount() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(new DefaultAccount(
+ AccountCategory.CLOUD, new Account("randomaccount1@gmail.com", "com.google")));
+
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_accountInValuesOnly() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts"); // No account in URI
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "test_account");
+ values.put(RawContacts.ACCOUNT_TYPE, "com.google");
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ AccountWithDataSet result1 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result1.getAccountName());
+ assertEquals("com.google", result1.getAccountType());
+ assertEquals("test_data_set", result1.getDataSet());
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ AccountWithDataSet result2 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+
+ assertEquals("test_account", result2.getAccountName());
+ assertEquals("com.google", result2.getAccountType());
+ assertEquals("test_data_set", result2.getDataSet());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_invalidAccountInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "invalid_account")
+ .build(); // Missing ACCOUNT_TYPE
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "test_account");
+ values.put(RawContacts.ACCOUNT_TYPE, "com.google");
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ when(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri))
+ .thenReturn("Test Exception Message");
+
+ // Expecting an exception due to the invalid account in the URI
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values,
+ /*applyDefaultAccount=*/false);
+ });
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ // Expecting an exception due to the invalid account in the URI
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values,
+ /*applyDefaultAccount=*/true);
+ });
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_invalidAccountInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "invalid_account"); // Invalid account
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ when(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri))
+ .thenReturn("Test Exception Message");
+
+ // Expecting an exception due to the invalid account in the values
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ // Expecting an exception due to the invalid account in the URI
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_matchingAccounts() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "test_account");
+ values.put(RawContacts.ACCOUNT_TYPE, "com.google");
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ AccountWithDataSet result1 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result1.getAccountName());
+ assertEquals("com.google", result1.getAccountType());
+ assertEquals("test_data_set", result1.getDataSet());
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+
+ AccountWithDataSet result2 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertEquals("test_account", result2.getAccountName());
+ assertEquals("com.google", result2.getAccountType());
+ assertEquals("test_data_set", result2.getDataSet());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_invalidAccountsBoth() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "invalid_account_uri")
+ .build(); // Missing ACCOUNT_TYPE
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "invalid_account_values");
+ values.put(RawContacts.DATA_SET, "test_data_set");
+
+ when(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri))
+ .thenReturn("Test Exception Message");
+
+ // Expecting an exception due to the invalid account in the URI
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+
+ // Expecting an exception due to the invalid account in the URI, regardless of what is the
+ // default account
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, new Account(
+ "test_account", "com.google"
+ )));
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_partialAccountInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account")
+ .build();
+ ContentValues values = new ContentValues();
+
+ when(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri))
+ .thenReturn("Test Exception Message");
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ // Expecting an exception due to the partial account in uri, regardless of what is the
+ // default account
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, new Account(
+ "test_account", "com.google"
+ )));
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_partialAccountInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "test_account");
+
+ when(mDbHelper.exceptionMessage(
+ "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri))
+ .thenReturn("Test Exception Message");
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ // Expecting an exception due to the partial account in uri, regardless of what is the
+ // default account
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, new Account(
+ "test_account", "com.google"
+ )));
+ exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_mismatchedAccounts() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "test_account_uri")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "com.google_uri")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "test_account_values");
+ values.put(RawContacts.ACCOUNT_TYPE, "com.google_values");
+
+ when(mDbHelper.exceptionMessage(
+ "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri))
+ .thenReturn("Test Exception Message");
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ // Expecting an exception due to the uri and content value's account info mismatching,
+ // regardless of what is the default account
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, new Account(
+ "test_account", "com.google"
+ )));
+ exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/false);
+ });
+ assertEquals("Test Exception Message", exception.getMessage());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_emptyAccountInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result); // Expect null result as account is effectively absent
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsDeviceOrUnknown_emptyAccountInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ AccountWithDataSet result1 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result1); // Expect null result as account is effectively absent
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ AccountWithDataSet result2 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result2); // Expect null result as account is effectively absent
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsCloud_emptyAccountInUri() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(AccountCategory.CLOUD,
+ new Account("test_user2", "com.google")));
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ assertEquals("Cannot write contacts to local accounts when default account is set to cloud",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_emptyAccountInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result); // Expect null result as account is effectively absent
+ }
+
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountDeviceOrUnknown_emptyAccountInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ AccountWithDataSet result1 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result1); // Expect null result as account is effectively absent
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ AccountWithDataSet result2 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result2); // Expect null result as account is effectively absent
+ }
+
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsCloud_emptyAccountInValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(AccountCategory.CLOUD,
+ new Account("test_user2", "com.google")));
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ assertEquals("Cannot write contacts to local accounts when default account is set to cloud",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_ignoreDefaultAccount_emptyAccountInUriAndValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result); // Expect null result as account is effectively absent
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultDeviceOrUnknown_emptyAccountInUriAndValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+ AccountWithDataSet result1 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result1); // Expect null result as account is effectively absent
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+ AccountWithDataSet result2 = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/true);
+ assertNull(result2); // Expect null result as account is effectively absent
+ }
+
+ @Test
+ public void testResolveAccountWithDataSet_defaultAccountIsCloud_emptyAccountInUriAndValues() {
+ Uri uri = Uri.parse("content://com.android.contacts/raw_contacts")
+ .buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "")
+ .build();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, "");
+ values.put(RawContacts.ACCOUNT_TYPE, "");
+
+ AccountWithDataSet result = mAccountResolver.resolveAccountWithDataSet(
+ uri, values, /*applyDefaultAccount=*/false);
+
+ assertNull(result); // Expect null result as account is effectively absent
+
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(AccountCategory.CLOUD,
+ new Account("test_user2", "com.google")));
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.resolveAccountWithDataSet(uri, values, /*applyDefaultAccount=*/true);
+ });
+ assertEquals("Cannot write contacts to local accounts when default account is set to cloud",
+ exception.getMessage());
+ }
+
+
+ @Test
+ public void testCheckAccountIsWritable_bothAccountNameAndTypeAreNullOrEmpty_NoException() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ mAccountResolver.checkAccountIsWritable("", "");
+ mAccountResolver.checkAccountIsWritable(null, "");
+ mAccountResolver.checkAccountIsWritable("", null);
+ mAccountResolver.checkAccountIsWritable(null, null);
+ // No exception expected
+ }
+
+ @Test
+ public void testCheckAccountIsWritable_eitherAccountNameOrTypeEmpty_ThrowsException() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.checkAccountIsWritable("accountName", "");
+ });
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.checkAccountIsWritable("accountName", null);
+ });
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.checkAccountIsWritable("", "accountType");
+ });
+ assertThrows(IllegalArgumentException.class, () -> {
+ mAccountResolver.checkAccountIsWritable(null, "accountType");
+ });
+ }
+
+ @Test
+ public void testCheckAccountIsWritable_defaultAccountIsCloud() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ new DefaultAccount(AccountCategory.CLOUD,
+ new Account("test_user1", "com.google")));
+
+ mAccountResolver.checkAccountIsWritable("test_user1", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user2", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user3", "com.whatsapp");
+ assertThrows(IllegalArgumentException.class, () ->
+ mAccountResolver.checkAccountIsWritable("", ""));
+ assertThrows(IllegalArgumentException.class, () ->
+ mAccountResolver.checkAccountIsWritable(null, null));
+ // No exception expected
+ }
+
+ @Test
+ public void testCheckAccountIsWritable_defaultAccountIsDevice() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT);
+
+ mAccountResolver.checkAccountIsWritable("test_user1", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user2", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user3", "com.whatsapp");
+ mAccountResolver.checkAccountIsWritable("", "");
+ mAccountResolver.checkAccountIsWritable(null, null);
+ // No exception expected
+ }
+
+
+ @Test
+ public void testCheckAccountIsWritable_defaultAccountIsUnknown() {
+ when(mDefaultAccountManager.pullDefaultAccount()).thenReturn(
+ DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT);
+
+ mAccountResolver.checkAccountIsWritable("test_user1", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user2", "com.google");
+ mAccountResolver.checkAccountIsWritable("test_user3", "com.whatsapp");
+ mAccountResolver.checkAccountIsWritable("", "");
+ mAccountResolver.checkAccountIsWritable(null, null);
+ // No exception expected
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 26e8aac..7e928eb 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -17,6 +17,7 @@
package com.android.providers.contacts;
import static android.content.pm.UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT;
+
import static com.android.providers.contacts.ContactsActor.MockUserManager.CLONE_PROFILE_USER;
import static com.android.providers.contacts.ContactsActor.MockUserManager.PRIMARY_USER;
@@ -386,7 +387,11 @@
@Override
public File getFilesDir() {
// TODO: Need to figure out something more graceful than this.
- return new File("/data/data/com.android.providers.contacts.tests/files");
+ // The returned file path must take into account the user under
+ // which the test is running given that in HSUM the test doesn't
+ // run under the system user.
+ return new File("/data/user/" + UserHandle.myUserId()
+ + "/com.android.providers.contacts.tests/files");
}
@Override
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
index 40c3b8a..9901fdb 100644
--- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
@@ -512,39 +512,56 @@
}
public void testGetAndSetDefaultAccount() {
- Account account = mDbHelper.getDefaultAccount();
- assertNull(account);
+ // Test: Initially, no default account exists
+ Account[] accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(0, accounts.length); // Check for empty array
+ // Test: Setting and getting valid default account
mDbHelper.setDefaultAccount("a", "b");
- account = mDbHelper.getDefaultAccount();
- assertEquals("a", account.name);
- assertEquals("b", account.type);
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(1, accounts.length);
+ assertEquals("a", accounts[0].name);
+ assertEquals("b", accounts[0].type);
mDbHelper.setDefaultAccount("c", "d");
- account = mDbHelper.getDefaultAccount();
- assertEquals("c", account.name);
- assertEquals("d", account.type);
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(1, accounts.length);
+ assertEquals("c", accounts[0].name);
+ assertEquals("d", accounts[0].type);
+ // Test: set the default account to NULL.
mDbHelper.setDefaultAccount(null, null);
- account = mDbHelper.getDefaultAccount();
- assertNull(account);
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(1, accounts.length);
+ assertNull(accounts[0]);
- // Invalid account (not-null account name and null account type) throws exception.
+ // Test: Invalid account (non-null name, null type)
try {
mDbHelper.setDefaultAccount("name", null);
fail("Setting default account to an invalid account should fail.");
} catch (IllegalArgumentException e) {
- // expected.
+ // Expected exception
}
- account = mDbHelper.getDefaultAccount();
- assertNull(account);
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(1, accounts.length);
+ assertNull(accounts[0]);
- // Update default account to an existing account
+ // Test: Update default account to an existing account
mDbHelper.setDefaultAccount("a", "b");
- account = mDbHelper.getDefaultAccount();
- assertEquals("a", account.name);
- assertEquals("b", account.type);
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(1, accounts.length);
+ assertEquals("a", accounts[0].name);
+ assertEquals("b", accounts[0].type);
+ // Test: Unset the default account.
+ ContentValues values = new ContentValues();
+ values.put(ContactsDatabaseHelper.AccountsColumns.IS_DEFAULT, 0);
+ mDb.update(Tables.ACCOUNTS, values, null, null);
+
+ accounts = mDbHelper.getDefaultAccountIfAny();
+ assertEquals(0, accounts.length); // Check for empty array
+
+ // Test: Verify total accounts in the database (including added defaults)
try (Cursor cursor = mDbHelper.getReadableDatabase().query(Tables.ACCOUNTS, new String[]{
ContactsDatabaseHelper.AccountsColumns.ACCOUNT_NAME,
ContactsDatabaseHelper.AccountsColumns.ACCOUNT_TYPE
@@ -552,4 +569,72 @@
assertEquals(3, cursor.getCount());
}
}
+
+ void createRawContact(AccountWithDataSet account) {
+ createRawContact(account, /* deleted= */ false);
+ }
+
+ void createRawContact(AccountWithDataSet account, boolean deleted) {
+ // Create an account.
+ final long accountId = mDbHelper.getOrCreateAccountIdInTransaction(account);
+ // Create a raw contact.
+ ContentValues rawContactValues = new ContentValues();
+ rawContactValues.put(ContactsDatabaseHelper.RawContactsColumns.ACCOUNT_ID, accountId);
+ if (deleted) {
+ rawContactValues.put(RawContactsColumns.CONCRETE_DELETED, 1);
+ }
+ mDb.insert(Tables.RAW_CONTACTS, null, rawContactValues);
+ }
+
+ public void testCountRawContactsForAccount() {
+ createRawContact(
+ new AccountWithDataSet("testName", "testType", /* dataSet= */ null));
+
+ int count = mDbHelper.countRawContactsQuery(Set.of(
+ new AccountWithDataSet("testName", "testType", /* dataSet= */ null)
+ ));
+
+ assertEquals(1, count);
+ }
+
+ public void testCountRawContactsForAccountsNullAccount() {
+ createRawContact(new AccountWithDataSet(null, null, null));
+ createRawContact(new AccountWithDataSet(null, null, null));
+
+ int count = mDbHelper.countRawContactsQuery(Set.of(
+ new AccountWithDataSet(null, null, null)
+ ));
+
+ assertEquals(2, count);
+ }
+
+ public void testCountRawContactsDoesNotIncludeDeletedContacts() {
+ createRawContact(new AccountWithDataSet(null, null, null));
+ createRawContact(new AccountWithDataSet(null, null, null),
+ /* deleted= */ true
+ );
+
+ int count = mDbHelper.countRawContactsQuery(Set.of(
+ new AccountWithDataSet(null, null, null)
+ ));
+
+ assertEquals(1, count);
+ }
+
+ public void testCountRawContactsForAccountsEmptyLocalAccount() {
+ int count = mDbHelper.countRawContactsQuery(Set.of(AccountWithDataSet.LOCAL));
+
+ assertEquals(0, count);
+ }
+
+ public void testCountRawContactsForUnrelatedAccount() {
+ createRawContact(
+ new AccountWithDataSet("testName", "testType", /* dataSet= */ null));
+
+ int count = mDbHelper.countRawContactsQuery(Set.of(
+ new AccountWithDataSet(null, null, null)
+ ));
+
+ assertEquals(0, count);
+ }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index d8d7ed3..8696c59 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -41,6 +41,9 @@
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds.Callable;
@@ -83,7 +86,8 @@
import android.text.TextUtils;
import android.util.ArraySet;
-import androidx.test.filters.LargeTest;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
import com.android.providers.contacts.ContactsActor.AlteringUserContext;
@@ -96,6 +100,7 @@
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.flags.Flags;
import com.android.providers.contacts.tests.R;
import com.android.providers.contacts.testutil.CommonDatabaseUtils;
import com.android.providers.contacts.testutil.ContactUtil;
@@ -111,6 +116,12 @@
import com.google.android.collect.Lists;
import com.google.android.collect.Sets;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -131,7 +142,8 @@
com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
* </code>
*/
-@LargeTest
+@RunWith(AndroidJUnit4.class)
+@UiThreadTest
public class ContactsProvider2Test extends BaseContactsProvider2Test {
private static final String TAG = ContactsProvider2Test.class.getSimpleName();
@@ -147,6 +159,9 @@
static final String TEST_PHONE_ACCOUNT_HANDLE_ICC_ID2 = "T5E5S5T5I5C5C5I5D";
static final String TEST_COMPONENT_NAME = "foo/bar";
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private int mOldMinMatch1;
private int mOldMinMatch2;
@@ -155,8 +170,9 @@
private ContactsDatabaseHelper mDbHelper;
private BroadcastReceiver mBroadcastReceiver;
+ @Before
@Override
- protected void setUp() throws Exception {
+ public void setUp() throws Exception {
super.setUp();
mContactsProvider2 = (ContactsProvider2) getProvider();
mDbHelper = mContactsProvider2.getThreadActiveDatabaseHelperForTest();
@@ -165,10 +181,12 @@
mOldMinMatch2 = mDbHelper.getMinMatchForTest();
PhoneNumberUtils.setMinMatchForTest(MIN_MATCH);
mDbHelper.setMinMatchForTest(MIN_MATCH);
+ assertNotNull(mDbHelper);
}
+ @After
@Override
- protected void tearDown() throws Exception {
+ public void tearDown() throws Exception {
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
//final ContactsDatabaseHelper dbHelper = cp.getThreadActiveDatabaseHelperForTest();
PhoneNumberUtils.setMinMatchForTest(mOldMinMatch1);
@@ -243,6 +261,7 @@
return contactsDatabaseHelper;
}
+ @Test
public void testPhoneAccountHandleMigrationSimEvent() throws IOException {
ContactsDatabaseHelper originalContactsDatabaseHelper
= mContactsProvider2.getContactsDatabaseHelperForTest();
@@ -302,6 +321,7 @@
mContactsProvider2.setContactsDatabaseHelperForTest(originalContactsDatabaseHelper);
}
+ @Test
public void testPhoneAccountHandleMigrationInitiation() throws IOException {
ContactsDatabaseHelper originalContactsDatabaseHelper
= mContactsProvider2.getContactsDatabaseHelperForTest();
@@ -357,6 +377,7 @@
mContactsProvider2.setContactsDatabaseHelperForTest(originalContactsDatabaseHelper);
}
+ @Test
public void testPhoneAccountHandleMigrationPendingStatus() {
// Mock ContactsDatabaseHelper
ContactsDatabaseHelper contactsDatabaseHelper = getMockContactsDatabaseHelper(
@@ -374,6 +395,7 @@
assertTrue(phoneAccountHandleMigrationUtils.isPhoneAccountMigrationPending());
}
+ @Test
public void testConvertEnterpriseUriWithEnterpriseDirectoryToLocalUri() {
String phoneNumber = "886";
String directory = String.valueOf(Directory.ENTERPRISE_DEFAULT);
@@ -391,6 +413,7 @@
assertUriEquals(expectedUri, localUri);
}
+ @Test
public void testConvertEnterpriseUriWithPersonalDirectoryToLocalUri() {
String phoneNumber = "886";
String directory = String.valueOf(Directory.DEFAULT);
@@ -408,6 +431,7 @@
assertUriEquals(expectedUri, localUri);
}
+ @Test
public void testConvertEnterpriseUriWithoutDirectoryToLocalUri() {
String phoneNumber = "886";
boolean isSip = true;
@@ -421,6 +445,7 @@
assertUriEquals(expectedUri, localUri);
}
+ @Test
public void testContactsProjection() {
assertProjection(Contacts.CONTENT_URI, new String[]{
Contacts._ID,
@@ -462,6 +487,7 @@
});
}
+ @Test
public void testContactsStrequentProjection() {
assertProjection(Contacts.CONTENT_STREQUENT_URI, new String[]{
Contacts._ID,
@@ -505,6 +531,7 @@
});
}
+ @Test
public void testContactsStrequentPhoneOnlyProjection() {
assertProjection(Contacts.CONTENT_STREQUENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true").build(),
@@ -555,6 +582,7 @@
});
}
+ @Test
public void testContactsWithSnippetProjection() {
assertProjection(Contacts.CONTENT_FILTER_URI.buildUpon().appendPath("nothing").build(),
new String[]{
@@ -598,6 +626,7 @@
});
}
+ @Test
public void testRawContactsProjection() {
assertProjection(RawContacts.CONTENT_URI, new String[]{
RawContacts._ID,
@@ -638,6 +667,7 @@
});
}
+ @Test
public void testDataProjection() {
assertProjection(Data.CONTENT_URI, new String[]{
Data._ID,
@@ -728,6 +758,7 @@
});
}
+ @Test
public void testDistinctDataProjection() {
assertProjection(Phone.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
new String[]{
@@ -809,6 +840,7 @@
});
}
+ @Test
public void testEntityProjection() {
assertProjection(
Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, 0),
@@ -907,6 +939,7 @@
});
}
+ @Test
public void testRawEntityProjection() {
assertProjection(RawContactsEntity.CONTENT_URI, new String[]{
RawContacts.Entity.DATA_ID,
@@ -958,6 +991,7 @@
});
}
+ @Test
public void testPhoneLookupProjection() {
assertProjection(PhoneLookup.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
new String[]{
@@ -993,6 +1027,7 @@
});
}
+ @Test
public void testPhoneLookupEnterpriseProjection() {
assertProjection(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI
.buildUpon().appendPath("123").build(),
@@ -1029,6 +1064,7 @@
});
}
+ @Test
public void testSipPhoneLookupProjection() {
assertContainProjection(PhoneLookup.CONTENT_FILTER_URI.buildUpon().appendPath("123")
.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, "1")
@@ -1058,6 +1094,7 @@
});
}
+ @Test
public void testSipPhoneLookupEnterpriseProjection() {
assertContainProjection(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI
.buildUpon()
@@ -1089,6 +1126,7 @@
});
}
+ @Test
public void testGroupsProjection() {
assertProjection(Groups.CONTENT_URI, new String[]{
Groups._ID,
@@ -1117,6 +1155,7 @@
});
}
+ @Test
public void testGroupsSummaryProjection() {
assertProjection(Groups.CONTENT_SUMMARY_URI, new String[]{
Groups._ID,
@@ -1148,6 +1187,7 @@
});
}
+ @Test
public void testAggregateExceptionProjection() {
assertProjection(AggregationExceptions.CONTENT_URI, new String[]{
AggregationExceptionColumns._ID,
@@ -1157,6 +1197,7 @@
});
}
+ @Test
public void testSettingsProjection() {
assertProjection(Settings.CONTENT_URI, new String[]{
Settings.ACCOUNT_NAME,
@@ -1170,6 +1211,7 @@
});
}
+ @Test
public void testStatusUpdatesProjection() {
assertProjection(StatusUpdates.CONTENT_URI, new String[]{
PresenceColumns.RAW_CONTACT_ID,
@@ -1188,6 +1230,7 @@
});
}
+ @Test
public void testDirectoryProjection() {
assertProjection(Directory.CONTENT_URI, new String[]{
Directory._ID,
@@ -1203,6 +1246,7 @@
});
}
+ @Test
public void testProviderStatusProjection() {
assertProjection(ProviderStatus.CONTENT_URI, new String[]{
ProviderStatus.STATUS,
@@ -1210,6 +1254,7 @@
});
}
+ @Test
public void testRawContactsInsert() {
ContentValues values = new ContentValues();
@@ -1241,6 +1286,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testDataDirectoryWithLookupUri() {
ContentValues values = new ContentValues();
@@ -1286,6 +1332,7 @@
cursor.close();
}
+ @Test
public void testContactEntitiesWithIdBasedUri() {
ContentValues values = new ContentValues();
Account account1 = new Account("act1", "actype1");
@@ -1308,6 +1355,7 @@
assertEntityRows(entityUri, contactId, rawContactId1, rawContactId2);
}
+ @Test
public void testContactEntitiesWithLookupUri() {
ContentValues values = new ContentValues();
Account account1 = new Account("act1", "actype1");
@@ -1404,6 +1452,7 @@
cursor.close();
}
+ @Test
public void testDataInsert() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
@@ -1432,6 +1481,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testDataInsertAndUpdateHashId() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
@@ -1489,6 +1539,7 @@
assertStoredValue(dataUri, Data.HASH_ID, null);
}
+ @Test
public void testDataInsertAndUpdateHashId_Photo() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
@@ -1513,6 +1564,7 @@
assertStoredValue(dataUri, Data.HASH_ID, hashId);
}
+ @Test
public void testDataInsertPhoneNumberTooLongIsTrimmed() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
@@ -1539,6 +1591,7 @@
assertSelection(dataUri, expected, Data._ID, dataId);
}
+ @Test
public void testRawContactDataQuery() {
Account account1 = new Account("a", "b");
Account account2 = new Account("c", "d");
@@ -1553,6 +1606,7 @@
assertStoredValue(uri2, Data._ID, ContentUris.parseId(dataUri2)) ;
}
+ @Test
public void testPhonesQuery() {
ContentValues values = new ContentValues();
@@ -1589,6 +1643,7 @@
assertStoredValues(ContentUris.withAppendedId(Phone.CONTENT_URI, phoneId), values);
}
+ @Test
public void testPhonesWithMergedContacts() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
insertPhoneNumber(rawContactId1, "123456789", true);
@@ -1628,6 +1683,7 @@
assertStoredValues(dedupeUri, values1);
}
+ @Test
public void testPhonesNormalizedNumber() {
final long rawContactId = RawContactUtil.createRawContact(mResolver);
@@ -1730,6 +1786,7 @@
);
}
+ @Test
public void testPhonesFilterQuery() {
testPhonesFilterQueryInter(Phone.CONTENT_FILTER_URI);
}
@@ -1839,6 +1896,7 @@
assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
}
+ @Test
public void testPhonesFilterSearchParams() {
final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "Dad", null);
insertPhoneNumber(rid1, "123-456-7890");
@@ -1874,6 +1932,7 @@
);
}
+ @Test
public void testPhoneLookup() {
ContentValues values = new ContentValues();
values.put(RawContacts.CUSTOM_RINGTONE, "d");
@@ -1913,6 +1972,7 @@
assertEquals(0, getCount(lookupUri2, null, null));
}
+ @Test
public void testSipPhoneLookup() {
ContentValues values = new ContentValues();
@@ -1942,6 +2002,7 @@
assertEquals(0, getCount(lookupUri2, null, null));
}
+ @Test
public void testPhoneLookupStarUseCases() {
// Create two raw contacts with numbers "*123" and "12 3". This is a real life example
// from b/13195334.
@@ -1975,6 +2036,7 @@
assertStoredValues(lookupUri, null, null, new ContentValues[] {values});
}
+ @Test
public void testPhoneLookupReturnsNothingRatherThanStar() {
// Create Emergency raw contact with "*123456789" number.
final ContentValues values = new ContentValues();
@@ -1989,6 +2051,7 @@
assertEquals(0, getCount(lookupUri, null, null));
}
+ @Test
public void testPhoneLookupReturnsNothingRatherThanMissStar() {
// Create Voice Mail raw contact with "123456789" number.
final ContentValues values = new ContentValues();
@@ -2003,6 +2066,7 @@
assertEquals(0, getCount(lookupUri, null, null));
}
+ @Test
public void testPhoneLookupStarNoFallbackMatch() {
final ContentValues values = new ContentValues();
final Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -2018,6 +2082,7 @@
assertEquals(0, getCount(lookupUri, null, null));
}
+ @Test
public void testPhoneLookupStarNotBreakFallbackMatching() {
// Create a raw contact with a phone number starting with "011"
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, new ContentValues());
@@ -2043,6 +2108,7 @@
assertStoredValues(lookupUri1, null, null, new ContentValues[]{values});
}
+ @Test
public void testPhoneLookupExplicitProjection() {
final ContentValues values = new ContentValues();
final Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -2067,6 +2133,7 @@
mResolver.query(lookupUri, projection, null, null, null);
}
+ @Test
public void testPhoneLookupUseCases() {
ContentValues values = new ContentValues();
Uri rawContactUri;
@@ -2146,6 +2213,7 @@
assertEquals(0, getCount(lookupUri2, null, null));
}
+ @Test
public void testIntlPhoneLookupUseCases() {
// Checks the logic that relies on phone_number_compare_loose(Gingerbread) as a fallback
//for phone number lookups.
@@ -2183,6 +2251,7 @@
PhoneLookup.CONTENT_FILTER_URI, "049102395"), null, null));
}
+ @Test
public void testPhoneLookupB5252190() {
// Test cases from b/5252190
String storedNumber = "796010101";
@@ -2210,6 +2279,7 @@
PhoneLookup.CONTENT_FILTER_URI, "4 879 601 0101"), null, null));
}
+ @Test
public void testPhoneLookupUseStrictPhoneNumberCompare() {
// Test lookup cases when mUseStrictPhoneNumberComparison is true
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
@@ -2270,6 +2340,7 @@
/**
* Test for enterprise caller-id, but with no corp profile.
*/
+ @Test
public void testPhoneLookupEnterprise_noCorpProfile() throws Exception {
Uri uri1 = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, "408-111-1111");
@@ -2298,6 +2369,7 @@
/**
* Test for enterprise caller-id. Corp profile exists, but it returns a null cursor.
*/
+ @Test
public void testPhoneLookupEnterprise_withCorpProfile_nullResult() throws Exception {
setUpNullCorpProvider();
@@ -2362,6 +2434,7 @@
* Note: in this test, we add one more provider instance for the authority
* "10@com.android.contacts" and use it as the corp cp2.
*/
+ @Test
public void testQueryMergedDataPhones() throws Exception {
mActor.addPermissions("android.permission.INTERACT_ACROSS_USERS");
@@ -2442,6 +2515,7 @@
* Note: in this test, we add one more provider instance for the authority
* "10@com.android.contacts" and use it as the corp cp2.
*/
+ @Test
public void testQueryMergedDataPhones_nullCorp() throws Exception {
mActor.addPermissions("android.permission.INTERACT_ACROSS_USERS");
@@ -2479,6 +2553,7 @@
* Note: in this test, we add one more provider instance for the authority
* "10@com.android.contacts" and use it as the corp cp2.
*/
+ @Test
public void testPhoneLookupEnterprise_withCorpProfile() throws Exception {
final SynchronousContactsProvider2 corpCp2 = setUpCorpProvider();
@@ -2569,6 +2644,7 @@
}
}
+ @Test
public void testQueryRawContactEntitiesCorp_noCorpProfile() {
mActor.addPermissions("android.permission.INTERACT_ACROSS_USERS");
@@ -2582,6 +2658,7 @@
assertEquals(0, getCount(RawContactsEntity.CORP_CONTENT_URI));
}
+ @Test
public void testQueryRawContactEntitiesCorp_withCorpProfile() throws Exception {
mActor.addPermissions("android.permission.INTERACT_ACROSS_USERS");
@@ -2625,6 +2702,7 @@
c.close();
}
+ @Test
public void testRewriteCorpDirectories() {
// 6 columns
final MatrixCursor c = new MatrixCursor(new String[] {
@@ -2665,6 +2743,7 @@
assertEquals("aname", rewritten.getString(column++));
}
+ @Test
public void testPhoneUpdate() {
ContentValues values = new ContentValues();
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -2703,6 +2782,7 @@
}
/** Tests if {@link Callable#CONTENT_URI} returns both phones and sip addresses. */
+ @Test
public void testCallablesQuery() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Meghan", "Knox");
long phoneId1 = ContentUris.parseId(insertPhoneNumber(rawContactId1, "18004664411"));
@@ -2735,10 +2815,12 @@
assertStoredValues(Callable.CONTENT_URI, new ContentValues[] { values1, values2 });
}
+ @Test
public void testCallablesFilterQuery() {
testPhonesFilterQueryInter(Callable.CONTENT_FILTER_URI);
}
+ @Test
public void testEmailsQuery() {
ContentValues values = new ContentValues();
values.put(RawContacts.CUSTOM_RINGTONE, "d");
@@ -2797,6 +2879,7 @@
assertStoredValues(dedupeUri, values);
}
+ @Test
public void testEmailsLookupQuery() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
insertEmail(rawContactId, "tamale@acme.com");
@@ -2817,6 +2900,7 @@
assertEquals(0, getCount(filterUri3, null, null));
}
+ @Test
public void testEmailsFilterQuery() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale",
TestUtil.ACCOUNT_1);
@@ -2852,6 +2936,7 @@
/**
* Tests if ContactsProvider2 returns addresses according to registration order.
*/
+ @Test
public void testEmailFilterDefaultSortOrder() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
insertEmail(rawContactId1, "address1@email.com");
@@ -2871,6 +2956,7 @@
/**
* Tests if ContactsProvider2 returns primary addresses before the other addresses.
*/
+ @Test
public void testEmailFilterPrimaryAddress() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
insertEmail(rawContactId1, "address1@email.com");
@@ -2888,6 +2974,7 @@
* Tests if ContactsProvider2 has email address associated with a primary account before the
* other address.
*/
+ @Test
public void testEmailFilterPrimaryAccount() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
insertEmail(rawContactId1, "account1@email.com");
@@ -2925,6 +3012,7 @@
/**
* Test emails with the same domain as primary account are ordered first.
*/
+ @Test
public void testEmailFilterSameDomainAccountOrder() {
final Account account = new Account("tester@email.com", "not_used");
final long rawContactId = RawContactUtil.createRawContact(mResolver, account);
@@ -2944,6 +3032,7 @@
/**
* Test "default" emails are sorted above emails used last.
*/
+ @Test
public void testEmailFilterSuperPrimaryOverUsageSort() {
final long rawContactId = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com");
@@ -2964,6 +3053,7 @@
assertStoredValuesOrderly(filterUri, v3, v1, v2);
}
+ @Test
public void testEmailFilterUsageOverPrimarySort() {
final long rawContactId = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com");
@@ -2985,6 +3075,7 @@
}
/** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */
+ @Test
public void testEmailFilterSortOrderWithFeedback() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
String address1 = "address1@email.com";
@@ -3037,6 +3128,7 @@
assertStoredValuesOrderly(filterUri3, new ContentValues[] { v1, v2, v3 });
}
+ @Test
public void testAddQueryParametersFromUri() {
final ContactsProvider2 provider = (ContactsProvider2) getProvider();
final Uri originalUri = Phone.CONTENT_FILTER_URI.buildUpon()
@@ -3059,6 +3151,7 @@
.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, directory).build();
}
+ @Test
public void testTestInvalidDirectory() throws Exception {
final ContactsProvider2 provider = (ContactsProvider2) getProvider();
assertTrue(provider.isDirectoryParamValid(Contacts.CONTENT_FILTER_URI));
@@ -3068,6 +3161,7 @@
assertFalse(provider.isDirectoryParamValid(buildContactsFilterUriWithDirectory("abc")));
}
+ @Test
public void testQueryCorpContactsProvider() throws Exception {
final ContactsProvider2 provider = (ContactsProvider2) getProvider();
final MockUserManager um = mActor.mockUserManager;
@@ -3113,6 +3207,7 @@
}
}
+ @Test
public void testPostalsQuery() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Alice", "Nextore");
Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
@@ -3158,6 +3253,7 @@
assertStoredValues(dedupeUri, values);
}
+ @Test
public void testDataContentUriInvisibleQuery() {
final ContentValues values = new ContentValues();
final long contactId = createContact(values, "John", "Doe",
@@ -3173,6 +3269,7 @@
assertEquals(0, getCount(uri, null, null));
}
+ @Test
public void testInDefaultDirectoryData() {
final ContentValues values = new ContentValues();
final long contactId = createContact(values, "John", "Doe",
@@ -3195,6 +3292,7 @@
getCount(Email.CONTENT_URI, query.toString(), new String[]{"goog411@acme.com"}));
}
+ @Test
public void testContactablesQuery() {
final long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot",
"Tamale");
@@ -3242,6 +3340,7 @@
assertStoredValues(filterUri7, cv1, cv2);
}
+ @Test
public void testContactablesMultipleQuery() {
final long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot",
@@ -3336,6 +3435,7 @@
}
+ @Test
public void testQueryContactData() {
ContentValues values = new ContentValues();
long contactId = createContact(values, "John", "Doe",
@@ -3347,6 +3447,7 @@
assertStoredValues(contactUri, values);
}
+ @Test
public void testQueryContactWithStatusUpdate() {
ContentValues values = new ContentValues();
long contactId = createContact(values, "John", "Doe",
@@ -3361,6 +3462,7 @@
assertStoredValuesWithProjection(contactUri, values);
}
+ @Test
public void testQueryContactFilterByName() {
ContentValues values = new ContentValues();
long rawContactId = createRawContact(values, "18004664411",
@@ -3401,6 +3503,7 @@
assertContactFilterNoResult("goolish");
}
+ @Test
public void testQueryContactFilterByEmailAddress() {
ContentValues values = new ContentValues();
long rawContactId = createRawContact(values, "18004664411",
@@ -3428,6 +3531,7 @@
assertContactFilterNoResult("goolish");
}
+ @Test
public void testQueryContactFilterByPhoneNumber() {
ContentValues values = new ContentValues();
long rawContactId = createRawContact(values, "18004664411",
@@ -3458,6 +3562,7 @@
* Checks ContactsProvider2 works well with strequent Uris. The provider should return starred
* contacts.
*/
+ @Test
public void testQueryContactStrequent() {
ContentValues values1 = new ContentValues();
final String email1 = "a@acme.com";
@@ -3513,6 +3618,7 @@
assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] { });
}
+ @Test
public void testQueryContactStrequentFrequentOrder() {
// Prepare test data
final long rid1 = RawContactUtil.createRawContact(mResolver);
@@ -3622,6 +3728,7 @@
* Checks ContactsProvider2 works well with frequent Uri. The provider should return frequently
* contacted person ordered by number of times contacted.
*/
+ @Test
public void testQueryContactFrequent() {
ContentValues values1 = new ContentValues();
final String email1 = "a@acme.com";
@@ -3681,6 +3788,7 @@
);
}
+ @Test
public void testQueryContactFrequentExcludingInvisible() {
ContentValues values1 = new ContentValues();
final String email1 = "a@acme.com";
@@ -3705,6 +3813,7 @@
}
+ @Test
public void testQueryDataUsageStat() {
// Now all data usage stats are zero as of Q.
@@ -3762,6 +3871,7 @@
assertDataUsageZero(dataUriWithUsageTypeCall, "a@acme.com");
}
+ @Test
public void testQueryContactGroup() {
long groupId = createGroup(null, "testGroup", "Test Group");
@@ -3824,6 +3934,7 @@
}
}
+ @Test
public void testQueryProfileWithoutPermission() {
createBasicProfileContact(new ContentValues());
@@ -3845,6 +3956,7 @@
.appendPath("entities").build(), null, null, null, Contacts._ID);
}
+ @Test
public void testQueryProfileByContactIdWithoutReadPermission() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
long profileContactId = queryContactId(profileRawContactId);
@@ -3856,6 +3968,7 @@
null, null, null, Contacts._ID);
}
+ @Test
public void testQueryProfileByRawContactIdWithoutReadPermission() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -3865,6 +3978,7 @@
profileRawContactId), null, null, null, RawContacts._ID);
}
+ @Test
public void testQueryProfileRawContactWithoutReadPermission() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -3888,6 +4002,7 @@
.appendPath("entity").build(), null, null, null, null);
}
+ @Test
public void testQueryProfileDataByDataIdWithoutReadPermission() {
createBasicProfileContact(new ContentValues());
Cursor c = mResolver.query(Profile.CONTENT_URI.buildUpon().appendPath("data").build(),
@@ -3903,6 +4018,7 @@
null, null, null, null);
}
+ @Test
public void testQueryProfileDataWithoutReadPermission() {
createBasicProfileContact(new ContentValues());
@@ -3912,6 +4028,7 @@
null, null, null, null);
}
+ @Test
public void testInsertProfileWithoutWritePermission() {
// Creating a non-profile contact should be fine.
createBasicNonProfileContact(new ContentValues());
@@ -3923,6 +4040,7 @@
}
}
+ @Test
public void testInsertProfileDataWithoutWritePermission() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -3933,6 +4051,7 @@
}
}
+ @Test
public void testUpdateDataDoesNotRequireProfilePermission() {
// Create a non-profile contact.
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Domo", "Arigato");
@@ -3952,6 +4071,7 @@
assertStoredValues(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), values);
}
+ @Test
public void testQueryContactThenProfile() {
ContentValues profileValues = new ContentValues();
long profileRawContactId = createBasicProfileContact(profileValues);
@@ -3969,6 +4089,7 @@
assertStoredValues(Profile.CONTENT_URI, profileValues);
}
+ @Test
public void testQueryContactExcludeProfile() {
// Create a profile contact (it should not be returned by the general contact URI).
createBasicProfileContact(new ContentValues());
@@ -3980,6 +4101,7 @@
assertStoredValues(Contacts.CONTENT_URI, new ContentValues[] {nonProfileValues});
}
+ @Test
public void testQueryProfile() {
ContentValues profileValues = new ContentValues();
createBasicProfileContact(profileValues);
@@ -4013,6 +4135,7 @@
return new ContentValues[]{photoRow, phoneRow, emailRow, nameRow};
}
+ @Test
public void testQueryProfileData() {
createBasicProfileContact(new ContentValues());
@@ -4020,6 +4143,7 @@
getExpectedProfileDataValues());
}
+ @Test
public void testQueryProfileEntities() {
createBasicProfileContact(new ContentValues());
@@ -4027,6 +4151,7 @@
getExpectedProfileDataValues());
}
+ @Test
public void testQueryRawProfile() {
ContentValues profileValues = new ContentValues();
createBasicProfileContact(profileValues);
@@ -4037,6 +4162,7 @@
assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, profileValues);
}
+ @Test
public void testQueryRawProfileById() {
ContentValues profileValues = new ContentValues();
long profileRawContactId = createBasicProfileContact(profileValues);
@@ -4048,6 +4174,7 @@
Profile.CONTENT_RAW_CONTACTS_URI, profileRawContactId), profileValues);
}
+ @Test
public void testQueryRawProfileData() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -4056,6 +4183,7 @@
.appendPath("data").build(), getExpectedProfileDataValues());
}
+ @Test
public void testQueryRawProfileEntity() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -4064,6 +4192,7 @@
.appendPath("entity").build(), getExpectedProfileDataValues());
}
+ @Test
public void testQueryDataForProfile() {
createBasicProfileContact(new ContentValues());
@@ -4071,6 +4200,7 @@
getExpectedProfileDataValues());
}
+ @Test
public void testUpdateProfileRawContact() {
createBasicProfileContact(new ContentValues());
ContentValues updatedValues = new ContentValues();
@@ -4082,6 +4212,7 @@
assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, updatedValues);
}
+ @Test
public void testInsertProfileWithDataSetTriggersAccountCreation() {
// Check that we have no profile raw contacts.
assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, new ContentValues[]{});
@@ -4100,6 +4231,7 @@
assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, values);
}
+ @Test
public void testLoadProfilePhoto() throws IOException {
long rawContactId = createBasicProfileContact(new ContentValues());
insertPhoto(rawContactId, R.drawable.earth_normal);
@@ -4108,6 +4240,7 @@
Contacts.openContactPhotoInputStream(mResolver, Profile.CONTENT_URI, false));
}
+ @Test
public void testLoadProfileDisplayPhoto() throws IOException {
long rawContactId = createBasicProfileContact(new ContentValues());
insertPhoto(rawContactId, R.drawable.earth_normal);
@@ -4116,6 +4249,7 @@
Contacts.openContactPhotoInputStream(mResolver, Profile.CONTENT_URI, true));
}
+ @Test
public void testPhonesWithStatusUpdate() {
ContentValues values = new ContentValues();
@@ -4168,6 +4302,7 @@
c.close();
}
+ @Test
public void testGroupQuery() {
Account account1 = new Account("a", "b");
Account account2 = new Account("c", "d");
@@ -4181,6 +4316,7 @@
assertStoredValue(uri2, Groups._ID + "=" + groupId2, null, Groups._ID, groupId2) ;
}
+ @Test
public void testGroupInsert() {
ContentValues values = new ContentValues();
@@ -4207,6 +4343,7 @@
assertStoredValues(rowUri, values);
}
+ @Test
public void testGroupCreationAfterMembershipInsert() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
Uri groupMembershipUri = insertGroupMembership(rawContactId1, "gsid1");
@@ -4216,6 +4353,7 @@
rawContactId1, groupId, "gsid1");
}
+ @Test
public void testGroupReuseAfterMembershipInsert() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
long groupId1 = createGroup(mAccount, "gsid1", "title1");
@@ -4226,6 +4364,7 @@
rawContactId1, groupId1, "gsid1");
}
+ @Test
public void testGroupInsertFailureOnGroupIdConflict() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
long groupId1 = createGroup(mAccount, "gsid1", "title1");
@@ -4243,6 +4382,7 @@
}
}
+ @Test
public void testGroupDelete_byAccountSelection() {
final Account account1 = new Account("accountName1", "accountType1");
final Account account2 = new Account("accountName2", "accountType2");
@@ -4271,6 +4411,7 @@
assertStoredValues(Groups.CONTENT_URI, new ContentValues[] { v1, v2, v3 });
}
+ @Test
public void testGroupDelete_byAccountParam() {
final Account account1 = new Account("accountName1", "accountType1");
final Account account2 = new Account("accountName2", "accountType2");
@@ -4302,6 +4443,7 @@
assertStoredValues(Groups.CONTENT_URI, new ContentValues[] { v1, v2, v3 });
}
+ @Test
public void testGroupSummaryQuery() {
final Account account1 = new Account("accountName1", "accountType1");
final Account account2 = new Account("accountName2", "accountType2");
@@ -4424,6 +4566,7 @@
assertStoredValuesWithProjection(uri, new ContentValues[] { v1, v2, v3, v4 });
}
+ @Test
public void testSettingsQuery() {
Account account1 = new Account("a", "b");
Account account2 = new Account("c", "d");
@@ -4449,6 +4592,7 @@
assertStoredValue(uri3, Settings.UNGROUPED_VISIBLE, "0");
}
+ @Test
public void testSettingsInsertionPreventsDuplicates() {
Account account1 = new Account("a", "b");
AccountWithDataSet account2 = new AccountWithDataSet("c", "d", "plus");
@@ -4468,6 +4612,7 @@
new String[] {"c", "d", "plus"}, Settings.SHOULD_SYNC, "0");
}
+ @Test
public void testSettingsDeletion() {
Account account = new Account("a", "b");
Uri settingUri = createSettings(account, "0", "1");
@@ -4490,6 +4635,7 @@
assertRowCount(0, Settings.CONTENT_URI, null, null);
}
+ @Test
public void testSettingsUpdate() {
Account account1 = new Account("a", "b");
Account account2 = new Account("c", "d");
@@ -4561,6 +4707,7 @@
));
}
+ @Test
public void testSettingsLocalAccount() {
AccountWithDataSet localAccount = AccountWithDataSet.LOCAL;
@@ -4598,6 +4745,7 @@
assertRowCount(1, Settings.CONTENT_URI, null, null);
}
+ @Test
public void testDisplayNameParsingWhenPartsUnspecified() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = new ContentValues();
@@ -4607,6 +4755,7 @@
assertStructuredName(rawContactId, "Mr.", "John", "Kevin", "von Smith", "Jr.");
}
+ @Test
public void testDisplayNameParsingWhenPartsAreNull() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = new ContentValues();
@@ -4617,6 +4766,7 @@
assertStructuredName(rawContactId, "Mr.", "John", "Kevin", "von Smith", "Jr.");
}
+ @Test
public void testDisplayNameParsingWhenPartsSpecified() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = new ContentValues();
@@ -4627,6 +4777,7 @@
assertStructuredName(rawContactId, null, null, null, "Johnson", null);
}
+ @Test
public void testContactWithoutPhoneticName() {
ContactLocaleUtils.setLocaleForTest(Locale.ENGLISH);
final long rawContactId = RawContactUtil.createRawContact(mResolver, null);
@@ -4672,6 +4823,7 @@
assertStoredValues(dataUri, values);
}
+ @Test
public void testContactWithChineseName() {
if (!hasChineseCollator()) {
return;
@@ -4718,6 +4870,7 @@
assertStoredValues(dataUri, values);
}
+ @Test
public void testJapaneseNameContactInEnglishLocale() {
// Need Japanese locale data for transliteration
if (!hasJapaneseCollator()) {
@@ -4739,6 +4892,7 @@
assertContactFilterNoResult("kong");
}
+ @Test
public void testContactWithJapaneseName() {
if (!hasJapaneseCollator()) {
return;
@@ -4790,6 +4944,7 @@
assertContactFilterNoResult("kong");
}
+ @Test
public void testDisplayNameUpdate() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
insertEmail(rawContactId1, "potato@acme.com", true);
@@ -4808,6 +4963,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testDisplayNameFromData() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -4844,6 +5000,7 @@
assertStoredValue(uri, Contacts.DISPLAY_NAME, "James P. Sullivan");
}
+ @Test
public void testDisplayNameFromOrganizationWithoutPhoneticName() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -4873,6 +5030,7 @@
assertStoredValues(uri, values);
}
+ @Test
public void testDisplayNameFromOrganizationWithJapanesePhoneticName() {
if (!hasJapaneseCollator()) {
return;
@@ -4901,6 +5059,7 @@
assertStoredValues(uri, values);
}
+ @Test
public void testDisplayNameFromOrganizationWithChineseName() {
if (!hasChineseCollator()) {
return;
@@ -4929,6 +5088,7 @@
assertStoredValues(uri, values);
}
+ @Test
public void testLookupByOrganization() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -4982,6 +5142,7 @@
assertEquals(0, getCount(filterUri, null, null));
}
+ @Test
public void testSearchSnippetOrganization() throws Exception {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5014,6 +5175,7 @@
assertContainsValues(filterUri, values);
}
+ @Test
public void testSearchSnippetEmail() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5030,6 +5192,7 @@
assertStoredValues(filterUri, values);
}
+ @Test
public void testCountPhoneNumberDigits() {
assertEquals(10, ContactsProvider2.countPhoneNumberDigits("86 (0) 5-55-12-34"));
assertEquals(10, ContactsProvider2.countPhoneNumberDigits("860 555-1234"));
@@ -5043,6 +5206,7 @@
assertEquals(0, ContactsProvider2.countPhoneNumberDigits("+441234098foo"));
}
+ @Test
public void testSearchSnippetPhone() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5088,6 +5252,7 @@
return values;
}
+ @Test
public void testSearchSnippetNickname() throws Exception {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5103,6 +5268,7 @@
assertStoredValues(filterUri, values);
}
+ @Test
public void testSearchSnippetEmptyForNameInDisplayName() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5115,6 +5281,7 @@
assertContainsValues(buildFilterUri("john", true), snippet);
}
+ @Test
public void testSearchSnippetEmptyForNicknameInDisplayName() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5126,6 +5293,7 @@
assertContainsValues(buildFilterUri("cave", true), snippet);
}
+ @Test
public void testSearchSnippetEmptyForCompanyInDisplayName() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5141,6 +5309,7 @@
assertContainsValues(buildFilterUri("aperture", true), snippet);
}
+ @Test
public void testSearchSnippetEmptyForPhoneInDisplayName() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5152,6 +5321,7 @@
assertContainsValues(buildFilterUri("860", true), snippet);
}
+ @Test
public void testSearchSnippetEmptyForEmailInDisplayName() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5164,6 +5334,7 @@
assertContainsValues(buildFilterUri("cave", true), snippet);
}
+ @Test
public void testDisplayNameUpdateFromStructuredNameUpdate() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
Uri nameUri = DataUtil.insertStructuredName(mResolver, rawContactId, "Slinky", "Dog");
@@ -5190,6 +5361,7 @@
assertStoredValue(uri, Contacts.DISPLAY_NAME, "Dog");
}
+ @Test
public void testInsertDataWithContentProviderOperations() throws Exception {
ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValues(new ContentValues())
@@ -5207,6 +5379,7 @@
assertStoredValue(uri, Contacts.DISPLAY_NAME, "John Doe");
}
+ @Test
public void testSendToVoicemailDefault() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5218,6 +5391,7 @@
c.close();
}
+ @Test
public void testSetSendToVoicemailAndRingtone() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -5238,6 +5412,7 @@
assertMetadataDirty(rawContactUri, false);
}
+ @Test
public void testSendToVoicemailAndRingtoneAfterAggregation() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "a", "b");
long contactId1 = queryContactId(rawContactId1);
@@ -5255,6 +5430,7 @@
assertSendToVoicemailAndRingtone(contactId1, true, "foo,bar"); // Either foo or bar
}
+ @Test
public void testDoNotSendToVoicemailAfterAggregation() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "e", "f");
long contactId1 = queryContactId(rawContactId1);
@@ -5272,6 +5448,7 @@
assertSendToVoicemailAndRingtone(queryContactId(rawContactId1), false, null);
}
+ @Test
public void testSetSendToVoicemailAndRingtonePreservedAfterJoinAndSplit() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "i", "j");
long contactId1 = queryContactId(rawContactId1);
@@ -5293,6 +5470,7 @@
assertSendToVoicemailAndRingtone(queryContactId(rawContactId2), false, "bar");
}
+ @Test
public void testMarkMetadataDirtyAfterAggregation() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "i", "j");
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "k", "l");
@@ -5314,6 +5492,7 @@
assertNetworkNotified(false);
}
+ @Test
public void testStatusUpdateInsert() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
Uri imUri = insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5367,6 +5546,7 @@
assertStoredValues(contactUri, values);
}
+ @Test
public void testStatusUpdateInferAttribution() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
Uri imUri = insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5388,6 +5568,7 @@
assertStoredValues(resultUri, values);
}
+ @Test
public void testStatusUpdateMatchingImOrEmail() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5432,6 +5613,7 @@
assertStoredValuesWithProjection(contactUri, values);
}
+ @Test
public void testStatusUpdateUpdateAndDelete() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5507,6 +5689,7 @@
assertStoredValuesWithProjection(contactUri, values);
}
+ @Test
public void testStatusUpdateUpdateToNull() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5535,6 +5718,7 @@
assertStoredValuesWithProjection(contactUri, values);
}
+ @Test
public void testStatusUpdateWithTimestamp() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
@@ -5568,6 +5752,7 @@
// Stream item query test cases.
+ @Test
public void testQueryStreamItemsByRawContactId() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
ContentValues values = buildGenericStreamItemValues();
@@ -5579,6 +5764,7 @@
values);
}
+ @Test
public void testQueryStreamItemsByContactId() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5591,6 +5777,7 @@
values);
}
+ @Test
public void testQueryStreamItemsByLookupKey() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5604,6 +5791,7 @@
values);
}
+ @Test
public void testQueryStreamItemsByLookupKeyAndContactId() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -5619,6 +5807,7 @@
values);
}
+ @Test
public void testQueryStreamItems() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5626,6 +5815,7 @@
assertStoredValues(StreamItems.CONTENT_URI, values);
}
+ @Test
public void testQueryStreamItemsWithSelection() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues firstValues = buildGenericStreamItemValues();
@@ -5644,6 +5834,7 @@
new String[]{"Goodbye world"}, secondValues);
}
+ @Test
public void testQueryStreamItemById() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues firstValues = buildGenericStreamItemValues();
@@ -5666,6 +5857,7 @@
// Stream item photo insertion + query test cases.
+ @Test
public void testQueryStreamItemPhotoWithSelection() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5683,6 +5875,7 @@
new String[]{"1"}, photo1Values);
}
+ @Test
public void testQueryStreamItemPhotoByStreamItemId() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
@@ -5714,6 +5907,7 @@
StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), photo2Values);
}
+ @Test
public void testQueryStreamItemPhotoByStreamItemPhotoId() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
@@ -5760,6 +5954,7 @@
// Stream item insertion test cases.
+ @Test
public void testInsertStreamItemInProfileRequiresWriteProfileAccess() {
long profileRawContactId = createBasicProfileContact(new ContentValues());
@@ -5768,6 +5963,7 @@
insertStreamItem(profileRawContactId, values, null);
}
+ @Test
public void testInsertStreamItemWithContentValues() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5778,6 +5974,7 @@
RawContacts.StreamItems.CONTENT_DIRECTORY), values);
}
+ @Test
public void testInsertStreamItemOverLimit() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5812,6 +6009,7 @@
assertEquals(1, streamItemIds.size());
}
+ @Test
public void testInsertStreamItemOlderThanOldestInLimit() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5835,6 +6033,7 @@
// Stream item photo insertion test cases.
+ @Test
public void testInsertStreamItemsAndPhotosInBatch() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues streamItemValues = buildGenericStreamItemValues();
@@ -5889,6 +6088,7 @@
// Stream item update test cases.
+ @Test
public void testUpdateStreamItemById() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5902,6 +6102,7 @@
RawContacts.StreamItems.CONTENT_DIRECTORY), values);
}
+ @Test
public void testUpdateStreamItemWithContentValues() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5917,6 +6118,7 @@
// Stream item photo update test cases.
+ @Test
public void testUpdateStreamItemPhotoById() throws IOException {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5945,6 +6147,7 @@
mResolver.openInputStream(Uri.parse(displayPhotoUri)));
}
+ @Test
public void testUpdateStreamItemPhotoWithContentValues() throws IOException {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues values = buildGenericStreamItemValues();
@@ -5974,6 +6177,7 @@
// Stream item deletion test cases.
+ @Test
public void testDeleteStreamItemById() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues firstValues = buildGenericStreamItemValues();
@@ -5994,6 +6198,7 @@
RawContacts.StreamItems.CONTENT_DIRECTORY), secondValues);
}
+ @Test
public void testDeleteStreamItemWithSelection() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
ContentValues firstValues = buildGenericStreamItemValues();
@@ -6015,6 +6220,7 @@
// Stream item photo deletion test cases.
+ @Test
public void testDeleteStreamItemPhotoById() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long streamItemId = ContentUris.parseId(
@@ -6039,6 +6245,7 @@
}
}
+ @Test
public void testDeleteStreamItemPhotoWithSelection() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long streamItemId = ContentUris.parseId(
@@ -6056,6 +6263,7 @@
assertStoredValues(photoUri, firstPhotoValues);
}
+ @Test
public void testDeleteStreamItemsWhenRawContactDeleted() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri streamItemUri = insertStreamItem(rawContactId,
@@ -6072,6 +6280,7 @@
assertStoredValues(streamItemPhotoUri, emptyValues);
}
+ @Test
public void testQueryStreamItemLimit() {
ContentValues values = new ContentValues();
values.put(StreamItems.MAX_ITEMS, 5);
@@ -6081,6 +6290,7 @@
// Tests for inserting or updating stream items as a side-effect of making status updates
// (forward-compatibility of status updates into the new social stream API).
+ @Test
public void testStreamItemInsertedOnStatusUpdate() {
// This method of creating a raw contact automatically inserts a status update with
@@ -6100,6 +6310,7 @@
expectedValues);
}
+ @Test
public void testStreamItemInsertedOnStatusUpdate_HtmlQuoting() {
// This method of creating a raw contact automatically inserts a status update with
@@ -6122,6 +6333,7 @@
expectedValues);
}
+ @Test
public void testStreamItemUpdatedOnSecondStatusUpdate() {
// This method of creating a raw contact automatically inserts a status update with
@@ -6161,6 +6373,7 @@
return values;
}
+ @Test
public void testSingleStatusUpdateRowPerContact() {
int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
String handle1 = "test@gmail.com";
@@ -6226,6 +6439,7 @@
c.close();
}
+ @Test
public void testContactVisibilityUpdateOnMembershipChange() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
assertVisibility(rawContactId, "0");
@@ -6254,6 +6468,7 @@
null, Contacts.IN_VISIBLE_GROUP, expectedValue);
}
+ @Test
public void testSupplyingBothValuesAndParameters() throws Exception {
Account account = new Account("account 1", "type%/:1");
Uri uri = ContactsContract.Groups.CONTENT_URI.buildUpon()
@@ -6286,6 +6501,7 @@
}
}
+ @Test
public void testContentEntityIterator() {
// create multiple contacts and check that the selected ones are returned
long id;
@@ -6375,6 +6591,7 @@
iterator.close();
}
+ @Test
public void testDataCreateUpdateDeleteByMimeType() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
@@ -6442,6 +6659,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testRawContactQuery() {
Account account1 = new Account("a", "b");
Account account2 = new Account("c", "d");
@@ -6461,6 +6679,7 @@
assertStoredValue(rowUri2, RawContacts._ID, rawContactId2) ;
}
+ @Test
public void testRawContactDeletion() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -6492,6 +6711,7 @@
assertNetworkNotified(false);
}
+ @Test
public void testRawContactDeletionKeepingAggregateContact() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, mAccount);
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, mAccount);
@@ -6507,6 +6727,7 @@
assertEquals(1, getCount(Contacts.CONTENT_URI, Contacts._ID + "=" + contactId, null));
}
+ @Test
public void testRawContactDeletion_byAccountParam() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -6543,6 +6764,7 @@
assertStoredValue(uri, RawContacts.DELETED, "1");
}
+ @Test
public void testRawContactDeletion_byAccountSelection() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -6568,6 +6790,7 @@
* Test for {@link ContactsProvider2#stringToAccounts} and
* {@link ContactsProvider2#accountsToString}.
*/
+ @Test
public void testAccountsToString() {
final Set<Account> EXPECTED_0 = Sets.newHashSet();
final Set<Account> EXPECTED_1 = Sets.newHashSet(TestUtil.ACCOUNT_1);
@@ -6614,6 +6837,7 @@
* Test for {@link ContactsProvider2#haveAccountsChanged} and
* {@link ContactsProvider2#saveAccounts}.
*/
+ @Test
public void testHaveAccountsChanged() {
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
@@ -6661,6 +6885,7 @@
assertTrue(cp.haveAccountsChanged(ACCOUNTS_1));
}
+ @Test
public void testAccountsUpdated() {
// This is to ensure we do not delete contacts with null, null (account name, type)
// accidentally.
@@ -6695,6 +6920,7 @@
+ rawContactId2, null));
}
+ @Test
public void testAccountDeletion() {
Account readOnlyAccount = new Account("act", READ_ONLY_ACCOUNT_TYPE);
ContactsProvider2 cp = (ContactsProvider2) getProvider();
@@ -6738,6 +6964,119 @@
Contacts.PHOTO_ID, ContentUris.parseId(photoUri1));
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_CP2_SYNC_SEARCH_INDEX_FLAG)
+ public void testSearchIndexUpdatedOnAccountDeletion() {
+ ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ SQLiteDatabase db = cp.getDatabaseHelper().getReadableDatabase();
+ mActor.setAccounts(new Account[]{mAccount});
+ cp.onAccountsUpdated(new Account[]{mAccount});
+
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Wick",
+ mAccount);
+
+ // Assert the contact is in the search index
+ assertStoredValue(buildFilterUri("wick", false), SearchSnippets.SNIPPET, null);
+ assertEquals(1, DatabaseUtils.longForQuery(db, "SELECT COUNT(*) FROM search_index", null));
+
+ // Remove the account
+ mActor.setAccounts(new Account[]{});
+ cp.onAccountsUpdated(new Account[]{});
+
+ // Assert the contact is no longer searchable
+ assertRowCount(0, buildFilterUri("wick", false), null, null);
+
+ // Assert the contact is no longer in the search index table
+ assertEquals(0, DatabaseUtils.longForQuery(db, "SELECT COUNT(*) FROM search_index", null));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_CP2_SYNC_SEARCH_INDEX_FLAG)
+ public void testSearchIndexUpdatedOnAccountDeletion_withMultipleAccounts() {
+ Account readOnlyAccount = new Account("act", READ_ONLY_ACCOUNT_TYPE);
+ ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ SQLiteDatabase db = cp.getDatabaseHelper().getReadableDatabase();
+ mActor.setAccounts(new Account[]{readOnlyAccount, mAccount});
+ cp.onAccountsUpdated(new Account[]{readOnlyAccount, mAccount});
+
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Wick",
+ readOnlyAccount);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "john", "wick",
+ mAccount);
+ insertEmail(rawContactId2, "person@movie.com", true);
+
+ assertAggregated(rawContactId1, rawContactId2);
+
+ // Assert the contact is searchable by name and email
+ assertStoredValue(buildFilterUri("wick", false), SearchSnippets.SNIPPET, null);
+ assertStoredValue(buildFilterUri("movie", false), SearchSnippets.SNIPPET,
+ "person@[movie].com");
+ // Since contacts are aggregated only 1 entry should be present in search index
+ assertEquals(1, DatabaseUtils.longForQuery(db, "SELECT COUNT(*) FROM search_index", null));
+
+ // Remove the writable account
+ mActor.setAccounts(new Account[]{readOnlyAccount});
+ cp.onAccountsUpdated(new Account[]{readOnlyAccount});
+
+ // Assert the contact is searchable by name but not by email
+ assertStoredValue(buildFilterUri("wick", false), SearchSnippets.SNIPPET, null);
+ assertRowCount(0, buildFilterUri("movie", false), null, null);
+
+ // Assert the contact is still in the search index table
+ assertEquals(1, DatabaseUtils.longForQuery(db, "SELECT count(*) FROM search_index", null));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_CP2_SYNC_SEARCH_INDEX_FLAG)
+ public void testSearchIndexUpdatedOnAccountDeletion_withMaxStaleContacts() {
+ Account readOnlyAccount = new Account("act", READ_ONLY_ACCOUNT_TYPE);
+ ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ SQLiteDatabase db = cp.getDatabaseHelper().getReadableDatabase();
+ mActor.setAccounts(new Account[]{readOnlyAccount, mAccount});
+ cp.onAccountsUpdated(new Account[]{readOnlyAccount, mAccount});
+
+ // The maximum amount of stale contacts before rebuilding search index completely
+ cp.setSearchIndexMaxUpdateFilterContacts(5);
+
+ // Add more contacts than the max amount of stale contacts, such that we trigger a
+ // rebuild of the search index during the account removal process
+ for (int i = 0; i < 10; i++) {
+ String firstName = "first" + i;
+ String lastName = "last" + i;
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, firstName,
+ lastName, readOnlyAccount);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, firstName,
+ lastName, mAccount);
+ insertEmail(rawContactId2, "person@corp" + i + ".com", true);
+
+ assertAggregated(rawContactId1, rawContactId2);
+
+ // Assert the contact is searchable by name and email
+ assertStoredValue(buildFilterUri(firstName, false), SearchSnippets.SNIPPET, null);
+ assertStoredValue(buildFilterUri("corp" + i, false), SearchSnippets.SNIPPET,
+ "person@[corp" + i + "].com");
+ // Since contacts are aggregated only 1 entry should be present in search index
+ assertEquals(i + 1,
+ DatabaseUtils.longForQuery(db, "SELECT COUNT(*) FROM search_index", null));
+ }
+
+ // Remove the writable account
+ mActor.setAccounts(new Account[]{readOnlyAccount});
+ cp.onAccountsUpdated(new Account[]{readOnlyAccount});
+
+ for (int i = 0; i < 10; i++) {
+ String firstName = "first" + i;
+ String lastName = "last" + i;
+ // Assert the contact is searchable by name but not by email
+ assertStoredValue(buildFilterUri(firstName, false), SearchSnippets.SNIPPET, null);
+ assertRowCount(0, buildFilterUri("corp" + i, false), null, null);
+ }
+
+ // Assert all of the contacts are still in the search index table
+ assertEquals(10, DatabaseUtils.longForQuery(db, "SELECT count(*) FROM search_index", null));
+ }
+
+ @Test
public void testStreamItemsCleanedUpOnAccountRemoval() {
Account doomedAccount = new Account("doom", "doom");
Account safeAccount = mAccount;
@@ -6780,6 +7119,7 @@
assertStoredValue(safeStreamItemPhotoUri, StreamItemPhotos._ID, safeStreamItemPhotoId);
}
+ @Test
public void testContactDeletion() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
TestUtil.ACCOUNT_1);
@@ -6796,6 +7136,7 @@
RawContacts.DELETED, "1");
}
+ @Test
public void testMarkAsDirtyParameter() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -6812,6 +7153,7 @@
assertNetworkNotified(false);
}
+ @Test
public void testDirtyWhenRawContactInsert() {
// When inserting a rawcontact.
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
@@ -6821,6 +7163,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testRawContactDirtyAndVersion() {
final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId);
@@ -6861,6 +7204,7 @@
assertEquals(version, getVersion(uri));
}
+ @Test
public void testRawContactClearDirty() {
final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
@@ -6876,6 +7220,7 @@
assertEquals(version, getVersion(uri));
}
+ @Test
public void testRawContactDeletionSetsDirty() {
final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
@@ -6892,6 +7237,7 @@
assertEquals(version, getVersion(uri));
}
+ @Test
public void testNotifyMetadataChangeForRawContactInsertBySyncAdapter() {
Uri uri = RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.name)
@@ -6904,6 +7250,7 @@
assertMetadataDirty(rawContactUri, false);
}
+ @Test
public void testMarkAsMetadataDirtyForRawContactMetadataChange() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
long contactId = queryContactId(rawContactId);
@@ -6934,6 +7281,7 @@
assertMetadataDirty(rawContactUri, false);
}
+ @Test
public void testMarkAsMetadataDirtyForRawContactBackupIdChange() {
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -6952,6 +7300,7 @@
assertMetadataDirty(rawContactUri, false);
}
+ @Test
public void testMarkAsMetadataDirtyForAggregationExceptionChange() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
long rawContactId2 = RawContactUtil.createRawContact(mResolver, new Account("b", "b"));
@@ -6965,6 +7314,7 @@
false);
}
+ @Test
public void testMarkAsMetadataNotDirtyForUsageStatsChange() {
final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "contact", "a");
final long did1a = ContentUris.parseId(insertEmail(rid1, "email_1_a@email.com"));
@@ -6974,6 +7324,7 @@
assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rid1), false);
}
+ @Test
public void testMarkAsMetadataDirtyForDataPrimarySettingInsert() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
Uri mailUri11 = insertEmail(rawContactId1, "test1@domain1.com", true, true);
@@ -6984,6 +7335,7 @@
false);
}
+ @Test
public void testMarkAsMetadataDirtyForDataPrimarySettingUpdate() {
long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com");
@@ -6999,6 +7351,7 @@
false);
}
+ @Test
public void testMarkAsMetadataDirtyForDataDelete() {
long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com", true, true);
@@ -7009,6 +7362,7 @@
false);
}
+ @Test
public void testDeleteContactWithoutName() {
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, new ContentValues());
long rawContactId = ContentUris.parseId(rawContactUri);
@@ -7023,6 +7377,7 @@
assertEquals(1, numDeleted);
}
+ @Test
public void testDeleteContactWithoutAnyData() {
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, new ContentValues());
long rawContactId = ContentUris.parseId(rawContactUri);
@@ -7035,6 +7390,7 @@
assertEquals(1, numDeleted);
}
+ @Test
public void testDeleteContactWithEscapedUri() {
ContentValues values = new ContentValues();
values.put(RawContacts.SOURCE_ID, "!@#$%^&*()_+=-/.,<>?;'\":[]}{\\|`~");
@@ -7047,6 +7403,7 @@
assertEquals(1, mResolver.delete(lookupUri, null, null));
}
+ @Test
public void testDeleteContactComposedOfSingleLocalRawContact() {
// Create a raw contact in the local (null) account
long rawContactId = RawContactUtil.createRawContact(mResolver, null);
@@ -7068,6 +7425,7 @@
c2.close();
}
+ @Test
public void testDeleteContactComposedOfTwoLocalRawContacts() {
// Create a raw contact in the local (null) account
long rawContactId1 = RawContactUtil.createRawContact(mResolver, null);
@@ -7104,6 +7462,7 @@
c3.close();
}
+ @Test
public void testDeleteContactComposedOfSomeLocalRawContacts() {
// Create a raw contact in the local (null) account
long rawContactId1 = RawContactUtil.createRawContact(mResolver, null);
@@ -7139,6 +7498,7 @@
c3.close();
}
+ @Test
public void testQueryContactWithEscapedUri() {
ContentValues values = new ContentValues();
values.put(RawContacts.SOURCE_ID, "!@#$%^&*()_+=-/.,<>?;'\":[]}{\\|`~");
@@ -7153,6 +7513,7 @@
c.close();
}
+ @Test
public void testGetPhotoUri() {
ContentValues values = new ContentValues();
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -7169,6 +7530,7 @@
Contacts.PHOTO_URI, photoUri);
}
+ @Test
public void testGetPhotoViaLookupUri() throws IOException {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7196,6 +7558,7 @@
thumbnail, mResolver.openInputStream(photoLookupUriWithoutId));
}
+ @Test
public void testInputStreamForPhoto() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7219,6 +7582,7 @@
mResolver.openInputStream(photoUri));
}
+ @Test
public void testSuperPrimaryPhoto() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
Uri photoUri1 = insertPhoto(rawContactId1, R.drawable.earth_normal);
@@ -7258,6 +7622,7 @@
assertStoredValue(contactUri, Contacts.PHOTO_ID, photoId1);
}
+ @Test
public void testUpdatePhoto() {
ContentValues values = new ContentValues();
Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -7285,6 +7650,7 @@
assertEquals(photoId, twigId);
}
+ @Test
public void testUpdateRawContactDataPhoto() {
// setup a contact with a null photo
ContentValues values = new ContentValues();
@@ -7320,6 +7686,7 @@
storedPhoto.close();
}
+ @Test
public void testOpenDisplayPhotoForContactId() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7332,6 +7699,7 @@
mResolver.openInputStream(photoUri));
}
+ @Test
public void testOpenDisplayPhotoForContactLookupKey() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7345,6 +7713,7 @@
mResolver.openInputStream(photoUri));
}
+ @Test
public void testOpenDisplayPhotoForContactLookupKeyAndId() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7359,6 +7728,7 @@
mResolver.openInputStream(photoUri));
}
+ @Test
public void testOpenDisplayPhotoForRawContactId() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
insertPhoto(rawContactId, R.drawable.earth_normal);
@@ -7370,6 +7740,7 @@
mResolver.openInputStream(photoUri));
}
+ @Test
public void testOpenDisplayPhotoByPhotoUri() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7384,6 +7755,7 @@
mResolver.openInputStream(Uri.parse(photoUri)));
}
+ @Test
public void testPhotoUriForDisplayPhoto() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7407,6 +7779,7 @@
photoUri);
}
+ @Test
public void testPhotoUriForThumbnailPhoto() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7436,6 +7809,7 @@
mResolver.openInputStream(Uri.parse(photoUri)));
}
+ @Test
public void testWriteNewPhotoToAssetFile() throws Exception {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7476,6 +7850,7 @@
mResolver.openInputStream(Uri.parse(thumbnailUri)));
}
+ @Test
public void testWriteUpdatedPhotoToAssetFile() throws Exception {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7533,6 +7908,7 @@
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])null).get();
}
+ @Test
public void testPhotoDimensionLimits() {
ContentValues values = new ContentValues();
values.put(DisplayPhoto.DISPLAY_MAX_DIM, 256);
@@ -7540,6 +7916,7 @@
assertStoredValues(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, values);
}
+ @Test
public void testPhotoStoreCleanup() throws IOException {
SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider;
PhotoStore photoStore = provider.getPhotoStore();
@@ -7634,6 +8011,7 @@
new ContentValues[0]);
}
+ @Test
public void testPhotoStoreCleanupForProfile() {
SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider;
PhotoStore profilePhotoStore = provider.getProfilePhotoStore();
@@ -7688,6 +8066,7 @@
}
+ @Test
public void testCleanupDanglingContacts_noDanglingContacts() throws Exception {
SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider;
RawContactUtil.createRawContactWithName(mResolver, "A", "B");
@@ -7703,6 +8082,7 @@
assertEquals(2, rawContactCursor.getCount());
}
+ @Test
public void testCleanupDanglingContacts_singleDanglingContacts() throws Exception {
SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider;
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "A", "B");
@@ -7717,6 +8097,7 @@
assertEquals(0, mResolver.query(Contacts.CONTENT_URI, null, null, null, null).getCount());
}
+ @Test
public void testCleanupDanglingContacts_multipleDanglingContacts() throws Exception {
SynchronousContactsProvider2 provider = (SynchronousContactsProvider2) mActor.provider;
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "A", "B");
@@ -7737,6 +8118,7 @@
assertEquals(1, mResolver.query(Contacts.CONTENT_URI, null, null, null, null).getCount());
}
+ @Test
public void testOverwritePhotoWithThumbnail() throws IOException {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7765,6 +8147,7 @@
mResolver.openInputStream(Uri.parse(photoUri)));
}
+ @Test
public void testUpdateRawContactSetStarred() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver);
Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
@@ -7814,6 +8197,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testUpdateContactOptionsSetStarred() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
long contactId = queryContactId(rawContactId);
@@ -7827,6 +8211,7 @@
assertNetworkNotified(true);
}
+ @Test
public void testSetAndClearSuperPrimaryEmail() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
Uri mailUri11 = insertEmail(rawContactId1, "test1@domain1.com");
@@ -7967,22 +8352,27 @@
assertStoredValue(mailUri2, Data.IS_SUPER_PRIMARY, withSuperPrimary ? 1 : 0);
}
+ @Test
public void testNewPrimaryInInsert() {
testChangingPrimary(false, false);
}
+ @Test
public void testNewPrimaryInInsertWithSuperPrimary() {
testChangingPrimary(false, true);
}
+ @Test
public void testNewPrimaryInUpdate() {
testChangingPrimary(true, false);
}
+ @Test
public void testNewPrimaryInUpdateWithSuperPrimary() {
testChangingPrimary(true, true);
}
+ @Test
public void testContactSortOrder() {
assertEquals(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + ", "
+ Contacts.SORT_KEY_PRIMARY,
@@ -8000,6 +8390,7 @@
+ suffix));
}
+ @Test
public void testContactCounts() {
Uri uri = Contacts.CONTENT_URI.buildUpon()
.appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
@@ -8044,6 +8435,7 @@
MoreAsserts.assertEquals(expected, actual);
}
+ @Test
public void testReadBooleanQueryParameter() {
assertBooleanUriParameter("foo:bar", "bool", true, true);
assertBooleanUriParameter("foo:bar", "bool", false, false);
@@ -8064,6 +8456,7 @@
Uri.parse(uriString), parameter, defaultValue));
}
+ @Test
public void testGetQueryParameter() {
assertQueryParameter("foo:bar", "param", null);
assertQueryParameter("foo:bar?param", "param", null);
@@ -8088,6 +8481,7 @@
assertQueryParameter("foo:bar?ppp=val&", "p", null);
}
+ @Test
public void testMissingAccountTypeParameter() {
// Try querying for RawContacts only using ACCOUNT_NAME
final Uri queryUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(
@@ -8100,6 +8494,7 @@
}
}
+ @Test
public void testInsertInconsistentAccountType() {
// Try inserting RawContact with inconsistent Accounts
final Account red = new Account("red", "red");
@@ -8119,10 +8514,12 @@
}
}
+ @Test
public void testProviderStatusNoContactsNoAccounts() throws Exception {
assertProviderStatus(ProviderStatus.STATUS_EMPTY);
}
+ @Test
public void testProviderStatusOnlyLocalContacts() throws Exception {
long rawContactId = RawContactUtil.createRawContact(mResolver);
assertProviderStatus(ProviderStatus.STATUS_NORMAL);
@@ -8131,6 +8528,7 @@
assertProviderStatus(ProviderStatus.STATUS_EMPTY);
}
+ @Test
public void testProviderStatusWithAccounts() throws Exception {
assertProviderStatus(ProviderStatus.STATUS_EMPTY);
mActor.setAccounts(new Account[]{TestUtil.ACCOUNT_1});
@@ -8150,6 +8548,7 @@
cursor.close();
}
+ @Test
public void testProperties() throws Exception {
ContactsProvider2 provider = (ContactsProvider2)getProvider();
ContactsDatabaseHelper helper = (ContactsDatabaseHelper)provider.getDatabaseHelper();
@@ -8164,6 +8563,7 @@
assertEquals("default", helper.getProperty("existent1", "default"));
}
+ @Test
public void testQueryMultiVCard() {
// No need to create any contacts here, because the query for multiple vcards
// does not go into the database at all
@@ -8180,6 +8580,7 @@
cursor.close();
}
+ @Test
public void testQueryFileSingleVCard() {
final VCardTestUriCreator contacts = createVCardTestContacts();
@@ -8204,6 +8605,7 @@
}
}
+ @Test
public void testQueryFileProfileVCard() {
createBasicProfileContact(new ContentValues());
Cursor cursor = mResolver.query(Profile.CONTENT_VCARD_URI, null, null, null, null);
@@ -8215,6 +8617,7 @@
cursor.close();
}
+ @Test
public void testOpenAssetFileMultiVCard() throws IOException {
final VCardTestUriCreator contacts = createVCardTestContacts();
@@ -8230,6 +8633,7 @@
assertTrue(data.contains("N:Doh;Jane;;;"));
}
+ @Test
public void testOpenAssetFileSingleVCard() throws IOException {
final VCardTestUriCreator contacts = createVCardTestContacts();
@@ -8259,6 +8663,7 @@
}
}
+ @Test
public void testAutoGroupMembership() {
long g1 = createGroup(mAccount, "g1", "t1", 0, true /* autoAdd */, false /* favorite */);
long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
@@ -8289,6 +8694,7 @@
}
}
+ @Test
public void testNoAutoAddMembershipAfterGroupCreation() {
long r1 = RawContactUtil.createRawContact(mResolver, mAccount);
long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
@@ -8313,6 +8719,7 @@
// the starred contacts should be added to group
// favorites group removed
// no change to starred status
+ @Test
public void testFavoritesMembershipAfterGroupCreation() {
long r1 = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.STARRED, "1");
long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
@@ -8385,6 +8792,7 @@
assertFalse(queryRawContactIsStarred(r7));
}
+ @Test
public void testFavoritesGroupMembershipChangeAfterStarChange() {
long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
@@ -8460,6 +8868,7 @@
assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
}
+ @Test
public void testStarChangedAfterGroupMembershipChange() {
long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
@@ -8530,6 +8939,7 @@
assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
}
+ @Test
public void testReadOnlyRawContact() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
@@ -8546,6 +8956,7 @@
assertStoredValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "third");
}
+ @Test
public void testReadOnlyDataRow() {
long rawContactId = RawContactUtil.createRawContact(mResolver);
Uri emailUri = insertEmail(rawContactId, "email");
@@ -8564,6 +8975,7 @@
assertStoredValue(emailUri, Email.ADDRESS, "changed");
}
+ @Test
public void testContactWithReadOnlyRawContact() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
@@ -8586,6 +8998,7 @@
assertStoredValue(rawContactUri2, RawContacts.CUSTOM_RINGTONE, "second");
}
+ @Test
public void testNameParsingQuery() {
Uri uri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name")
.appendQueryParameter(StructuredName.DISPLAY_NAME, "Mr. John Q. Doe Jr.").build();
@@ -8603,6 +9016,7 @@
cursor.close();
}
+ @Test
public void testNameConcatenationQuery() {
Uri uri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name")
.appendQueryParameter(StructuredName.PREFIX, "Mr")
@@ -8625,6 +9039,7 @@
cursor.close();
}
+ @Test
public void testBuildSingleRowResult() {
checkBuildSingleRowResult(
new String[] {"b"},
@@ -8672,6 +9087,7 @@
}
}
+ @Test
public void testDataUsageFeedbackAndDelete() {
sMockClock.install();
@@ -8813,6 +9229,7 @@
/*******************************************************
* Delta api tests.
*/
+ @Test
public void testContactDelete_hasDeleteLog() {
sMockClock.install();
long start = sMockClock.currentTimeMillis();
@@ -8823,6 +9240,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testContactDelete_marksRawContactsForDeletion() {
DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete(mAccount);
@@ -8837,6 +9255,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testContactDelete_checkRawContactContactId() {
DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete(mAccount);
@@ -8849,6 +9268,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testContactUpdate_metadataChange() {
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, ids.mRawContactId);
@@ -8864,6 +9284,7 @@
assertNetworkNotified(false);
}
+ @Test
public void testContactUpdate_updatesContactUpdatedTimestamp() {
sMockClock.install();
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8884,6 +9305,7 @@
}
// This implicitly tests the Contact create case.
+ @Test
public void testRawContactCreate_updatesContactUpdatedTimestamp() {
long startTime = System.currentTimeMillis();
@@ -8896,6 +9318,7 @@
RawContactUtil.delete(mResolver, rawContactId, true);
}
+ @Test
public void testRawContactUpdate_updatesContactUpdatedTimestamp() {
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8912,6 +9335,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testRawContactPsuedoDelete_hasDeleteLogForContact() {
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8925,6 +9349,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testRawContactDelete_hasDeleteLogForContact() {
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8945,6 +9370,7 @@
return ContactUtil.queryContactLastUpdatedTimestamp(mResolver, contactId);
}
+ @Test
public void testDataInsert_updatesContactLastUpdatedTimestamp() {
sMockClock.install();
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8960,6 +9386,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testDataDelete_updatesContactLastUpdatedTimestamp() {
sMockClock.install();
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -8978,6 +9405,7 @@
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
+ @Test
public void testDataUpdate_updatesContactLastUpdatedTimestamp() {
sMockClock.install();
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9003,6 +9431,7 @@
return ContentUris.parseId(uri);
}
+ @Test
public void testDeletedContactsDelete_isUnsupported() {
final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
DatabaseAsserts.assertDeleteIsUnsupported(mResolver, URI);
@@ -9011,12 +9440,14 @@
DatabaseAsserts.assertDeleteIsUnsupported(mResolver, uri);
}
+ @Test
public void testDeletedContactsInsert_isUnsupported() {
final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
DatabaseAsserts.assertInsertIsUnsupported(mResolver, URI);
}
+ @Test
public void testQueryDeletedContactsByContactId() {
DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete();
@@ -9024,6 +9455,7 @@
DeletedContactUtil.queryDeletedTimestampForContactId(mResolver, ids.mContactId));
}
+ @Test
public void testQueryDeletedContactsAll() {
final int numDeletes = 10;
@@ -9040,6 +9472,7 @@
assertEquals(numDeletes, endCount - startCount);
}
+ @Test
public void testQueryDeletedContactsSinceTimestamp() {
sMockClock.install();
@@ -9110,6 +9543,7 @@
/*******************************************************
* Pinning support tests
*/
+ @Test
public void testPinnedPositionsUpdate() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContact(mResolver);
final DatabaseAsserts.ContactIdPair i2 = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9181,6 +9615,7 @@
);
}
+ @Test
public void testPinnedPositionsAfterJoinAndSplit() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContactWithName(
mResolver, "A", "Smith");
@@ -9308,6 +9743,7 @@
);
}
+ @Test
public void testDefaultAccountSet_throwException() {
mActor.setAccounts(new Account[]{mAccount});
try {
@@ -9349,6 +9785,7 @@
}
}
+ @Test
public void testDefaultAccountSetAndQuery() {
Bundle response = mResolver.call(ContactsContract.AUTHORITY_URI,
Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null, null);
@@ -9381,6 +9818,7 @@
assertNull(account);
}
+ @Test
public void testPinnedPositionsDemoteIllegalArguments() {
try {
mResolver.call(ContactsContract.AUTHORITY_URI, PinnedPositions.UNDEMOTE_METHOD,
@@ -9408,6 +9846,7 @@
null);
}
+ @Test
public void testPinnedPositionsAfterDemoteAndUndemote() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContact(mResolver);
final DatabaseAsserts.ContactIdPair i2 = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9458,6 +9897,7 @@
* Verifies that any existing pinned contacts have their pinned positions incremented by one
* after the upgrade step
*/
+ @Test
public void testPinnedPositionsUpgradeTo906_PinnedContactsIncrementedByOne() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContact(mResolver);
final DatabaseAsserts.ContactIdPair i2 = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9484,6 +9924,7 @@
* Verifies that any unpinned contacts (or those with pinned position Integer.MAX_VALUE - 1)
* have their pinned positions correctly set to 0 after the upgrade step.
*/
+ @Test
public void testPinnedPositionsUpgradeTo906_UnpinnedValueCorrectlyUpdated() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContact(mResolver);
final DatabaseAsserts.ContactIdPair i2 = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9508,6 +9949,7 @@
* Tests the functionality of the
* {@link ContactsContract.PinnedPositions#pin(ContentResolver, long, int)} API.
*/
+ @Test
public void testPinnedPositions_ContactsContractPinnedPositionsPin() {
final DatabaseAsserts.ContactIdPair i1 = DatabaseAsserts.assertAndCreateContact(mResolver);
@@ -9540,6 +9982,7 @@
* End pinning support tests
******************************************************/
+ @Test
public void testAuthorization_authorize() throws Exception {
// Setup
ContentValues values = new ContentValues();
@@ -9558,6 +10001,7 @@
assertTrue(cp.isValidPreAuthorizedUri(authorizedUri));
}
+ @Test
public void testAuthorization_unauthorized() throws Exception {
// Setup
ContentValues values = new ContentValues();
@@ -9570,6 +10014,7 @@
assertFalse(cp.isValidPreAuthorizedUri(contactUri));
}
+ @Test
public void testAuthorization_invalidAuthorization() throws Exception {
// Setup
ContentValues values = new ContentValues();
@@ -9586,6 +10031,7 @@
assertFalse(cp.isValidPreAuthorizedUri(almostAuthorizedUri));
}
+ @Test
public void testAuthorization_expired() throws Exception {
// Setup
ContentValues values = new ContentValues();
@@ -9603,6 +10049,7 @@
assertFalse(cp.isValidPreAuthorizedUri(authorizedUri));
}
+ @Test
public void testAuthorization_contactUpgrade() throws Exception {
ContactsDatabaseHelper helper =
((ContactsDatabaseHelper) ((ContactsProvider2) getProvider()).getDatabaseHelper());
diff --git a/tests/src/com/android/providers/contacts/DefaultAccountManagerTest.java b/tests/src/com/android/providers/contacts/DefaultAccountManagerTest.java
new file mode 100644
index 0000000..bb9a1b1
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/DefaultAccountManagerTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static org.mockito.Mockito.argThat;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+public class DefaultAccountManagerTest extends BaseContactsProvider2Test {
+ private static final String TAG = "DefaultAccountManagerTest";
+ private static final Account SYSTEM_CLOUD_ACCOUNT_1 = new Account("user1@gmail.com",
+ "com.google");
+ private static final Account NON_SYSTEM_CLOUD_ACCOUNT_1 = new Account("user2@whatsapp.com",
+ "com.whatsapp");
+
+ private ContactsDatabaseHelper mDbHelper;
+ private DefaultAccountManager mDefaultAccountManager;
+ private SyncSettingsHelper mSyncSettingsHelper;
+ private AccountManager mMockAccountManager;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mDbHelper = getContactsProvider().getDatabaseHelper();
+ mSyncSettingsHelper = new SyncSettingsHelper();
+ mMockAccountManager = Mockito.mock(AccountManager.class);
+ mDefaultAccountManager = new DefaultAccountManager(getContactsProvider().getContext(),
+ mDbHelper, mSyncSettingsHelper, mMockAccountManager); // Inject mockAccountManager
+
+ setAccounts(new Account[0]);
+ DefaultAccountManager.setEligibleSystemCloudAccountTypesForTesting(
+ new String[]{SYSTEM_CLOUD_ACCOUNT_1.type});
+ }
+
+ private void setAccounts(Account[] accounts) {
+ Mockito.when(mMockAccountManager.getAccounts()).thenReturn(accounts);
+
+ // Construsts a map between the account type and account list, so that we could mock
+ // mMockAccountManager.getAccountsByType below.
+ Map<String, List<Account>> accountTypeMap = new HashMap<>();
+ for (Account account : accounts) {
+ if (accountTypeMap.containsKey(account.type)) {
+ accountTypeMap.get(account.type).add(account);
+ } else {
+ List<Account> accountList = new ArrayList<>();
+ accountList.add(account);
+ accountTypeMap.put(account.type, accountList);
+ }
+ }
+
+ // By default: getAccountsByType returns empty account list unless there is a match in
+ // in accountTypeMap.
+ Mockito.when(mMockAccountManager.getAccountsByType(
+ argThat(str -> !accountTypeMap.containsKey(str)))).thenReturn(new Account[0]);
+
+ for (Map.Entry<String, List<Account>> entry : accountTypeMap.entrySet()) {
+ String accountType = entry.getKey();
+ Mockito.when(mMockAccountManager.getAccountsByType(accountType)).thenReturn(
+ entry.getValue().toArray(new Account[0]));
+ }
+ }
+
+ public void testPushDca_noCloudAccountsSignedIn() {
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Push the DCA which is device account, which should succeed.
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT));
+ assertEquals(DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Push the DCA which is not signed in, expect failure.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ }
+
+ public void testPushDeviceAccountAsDca_cloudSyncIsOff() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+
+ // The initial DCA should be unknown, regardless of the cloud account existence and their
+ // sync status.
+ mSyncSettingsHelper.turnOffSync(SYSTEM_CLOUD_ACCOUNT_1);
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA as DEVICE account, which should succeed
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT));
+ assertEquals(DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA as the system cloud account which sync is currently off, should fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ assertTrue(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+ }
+
+ public void testPushCustomizedDeviceAccountAsDca_cloudSyncIsOff() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+ mSyncSettingsHelper.turnOffSync(SYSTEM_CLOUD_ACCOUNT_1);
+
+ // No cloud account remains sync on, and thus DCA reverts to the DEVICE.
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set DCA to be device account, which should succeed.
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT));
+ assertEquals(DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set DCA to be a system cloud account which sync is off, should fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ // Sync state should still remains off.
+ assertTrue(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+ }
+
+ public void testPushDca_dcaWasUnknown_tryPushDeviceAndThenCloudAccount() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+ mSyncSettingsHelper.turnOnSync(SYSTEM_CLOUD_ACCOUNT_1);
+
+ // 1 system cloud account with sync on. DCA was set to cloud before, and thus it's in
+ // a UNKNOWN state.
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA to be local, which should succeed. In addition, it should turn
+ // all system cloud account's sync off.
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT));
+ assertEquals(DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ // Sync setting should remain to be on.
+ assertFalse(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+
+ // Try to set the DCA to be system cloud account, which should succeed.
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+ // Sync setting should remain to be on.
+ assertFalse(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+ }
+
+ public void testPushDca_dcaWasCloud() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+ mSyncSettingsHelper.turnOnSync(SYSTEM_CLOUD_ACCOUNT_1);
+
+ // DCA was a system cloud initially.
+ mDbHelper.setDefaultAccount(SYSTEM_CLOUD_ACCOUNT_1.name, SYSTEM_CLOUD_ACCOUNT_1.type);
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set DCA to a device (null) account, which should succeed, and it shouldn't
+ // change the cloud account's sync status.
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT));
+ assertEquals(
+ DefaultAccount.DEVICE_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ assertFalse(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+
+ // Try to set DCA to the same system cloud account again, which should succeed
+ assertTrue(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+ assertFalse(mSyncSettingsHelper.isSyncOff(SYSTEM_CLOUD_ACCOUNT_1));
+ }
+
+ public void testPushDca_dcaWasUnknown_tryPushAccountNotSignedIn() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA to be an account not signed in, which should fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(new Account("unknown1@gmail.com", "com.google"))));
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ }
+
+ public void testPushDca_dcaWasUnknown_tryPushNonSystemCloudAccount() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1, NON_SYSTEM_CLOUD_ACCOUNT_1});
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA to be an account which is not a system cloud account, which should
+ // fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(NON_SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(DefaultAccount.UNKNOWN_DEFAULT_ACCOUNT,
+ mDefaultAccountManager.pullDefaultAccount());
+ }
+
+ public void testPushDca_dcaWasCloud_tryPushAccountNotSignedIn() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1});
+ mDbHelper.setDefaultAccount(SYSTEM_CLOUD_ACCOUNT_1.name, SYSTEM_CLOUD_ACCOUNT_1.type);
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA to be an account not signed in, which should fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(new Account("unknown1@gmail.com", "com.google"))));
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+ }
+
+ public void testPushDca_dcaWasCloud_tryPushNonSystemCloudAccount() {
+ setAccounts(new Account[]{SYSTEM_CLOUD_ACCOUNT_1, NON_SYSTEM_CLOUD_ACCOUNT_1});
+ mDbHelper.setDefaultAccount(SYSTEM_CLOUD_ACCOUNT_1.name, SYSTEM_CLOUD_ACCOUNT_1.type);
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+
+ // Try to set the DCA to be an account which is not a system cloud account, which should
+ // fail.
+ assertFalse(mDefaultAccountManager.tryPushDefaultAccount(
+ DefaultAccount.ofCloud(NON_SYSTEM_CLOUD_ACCOUNT_1)));
+ assertEquals(
+ new DefaultAccount(DefaultAccount.AccountCategory.CLOUD, SYSTEM_CLOUD_ACCOUNT_1),
+ mDefaultAccountManager.pullDefaultAccount());
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/MoveRawContactsTest.java b/tests/src/com/android/providers/contacts/MoveRawContactsTest.java
new file mode 100644
index 0000000..f4ce0dc
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/MoveRawContactsTest.java
@@ -0,0 +1,1151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static android.provider.ContactsContract.SimAccount.SDN_EF_TYPE;
+
+import static org.mockito.ArgumentMatchers.argThat;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+
+import androidx.test.filters.MediumTest;
+
+import com.android.providers.contacts.flags.Flags;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link ContactsProvider2} Move API.
+ *
+ * Run the test like this:
+ * <code>
+ adb shell am instrument -e class com.android.providers.contacts.MoveRawContactsTest -w \
+ com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+@RunWith(JUnit4.class)
+public class MoveRawContactsTest extends BaseContactsProvider2Test {
+ @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+
+ @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+ static final Account SOURCE_ACCOUNT = new Account("sourceName", "sourceType");
+ static final Account DEST_ACCOUNT = new Account("destName", "destType");
+ static final Account DEST_ACCOUNT_WITH_SOURCE_TYPE = new Account("destName", "sourceType");
+ static final Account DEST_CLOUD_ACCOUNT = new Account("destName", "com.google");
+ static final Account SIM_ACCOUNT = new Account("simName", "simType");
+
+ static final String SOURCE_ID = "uniqueSourceId";
+
+ static final String NON_PORTABLE_MIMETYPE = "test/mimetype";
+
+ static final String RES_PACKAGE = "testpackage";
+
+ ContactsProvider2 mCp;
+ AccountWithDataSet mSource;
+ AccountWithDataSet mDest;
+ AccountWithDataSet mCloudDest;
+ AccountWithDataSet mSimAcct;
+ ContactMover mMover;
+ DefaultAccountManager mDefaultAccountManager;
+ AccountManager mMockAccountManager;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mCp = (ContactsProvider2) getProvider();
+ mMockAccountManager = Mockito.mock(AccountManager.class);
+ mDefaultAccountManager = new DefaultAccountManager(mCp.getContext(),
+ mCp.getDatabaseHelper(), new SyncSettingsHelper(), mMockAccountManager);
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT, DEST_ACCOUNT});
+ mSource = AccountWithDataSet.get(SOURCE_ACCOUNT.name, SOURCE_ACCOUNT.type, null);
+ mDest = AccountWithDataSet.get(DEST_ACCOUNT.name, DEST_ACCOUNT.type, null);
+ mCloudDest = AccountWithDataSet.get(
+ DEST_CLOUD_ACCOUNT.name, DEST_CLOUD_ACCOUNT.type, null);
+ DefaultAccountManager.setEligibleSystemCloudAccountTypesForTesting(new String[]{
+ DEST_CLOUD_ACCOUNT.type,
+ });
+
+ mMover = new ContactMover(mCp, mCp.getDatabaseHelper(), mDefaultAccountManager);
+ mSimAcct = createSimAccount(SIM_ACCOUNT);
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private AccountWithDataSet createSimAccount(Account account) {
+ AccountWithDataSet accountWithDataSet =
+ new AccountWithDataSet(account.name, account.type, null);
+ final SQLiteDatabase db = mCp.getDatabaseHelper().getWritableDatabase();
+ db.beginTransaction();
+ try {
+ mCp.getDatabaseHelper()
+ .createSimAccountIdInTransaction(accountWithDataSet, 1, SDN_EF_TYPE);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return accountWithDataSet;
+ }
+
+ private void setDefaultAccountManagerAccounts(Account[] accounts) {
+ Mockito.when(mMockAccountManager.getAccounts()).thenReturn(accounts);
+
+ // Constructs a map between the account type and account list, so that we could mock
+ // mMockAccountManager.getAccountsByType below.
+ Map<String, List<Account>> accountTypeMap = new HashMap<>();
+ for (Account account : accounts) {
+ if (accountTypeMap.containsKey(account.type)) {
+ accountTypeMap.get(account.type).add(account);
+ } else {
+ List<Account> accountList = new ArrayList<>();
+ accountList.add(account);
+ accountTypeMap.put(account.type, accountList);
+ }
+ }
+
+ // By default: getAccountsByType returns empty account list unless there is a match in
+ // in accountTypeMap.
+ Mockito.when(mMockAccountManager.getAccountsByType(
+ argThat(str -> !accountTypeMap.containsKey(str)))).thenReturn(new Account[0]);
+
+ for (Map.Entry<String, List<Account>> entry : accountTypeMap.entrySet()) {
+ String accountType = entry.getKey();
+ Mockito.when(mMockAccountManager.getAccountsByType(accountType)).thenReturn(
+ entry.getValue().toArray(new Account[0]));
+ }
+ }
+
+ private void assertMovedContactIsDeleted(long rawContactId,
+ AccountWithDataSet account) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(RawContacts._ID, rawContactId);
+ contentValues.put(RawContacts.DELETED, 1);
+ contentValues.put(RawContacts.ACCOUNT_NAME, account.getAccountName());
+ contentValues.put(RawContacts.ACCOUNT_TYPE, account.getAccountType());
+ assertStoredValues(RawContacts.CONTENT_URI,
+ RawContacts._ID + " = ?",
+ new String[]{String.valueOf(rawContactId)},
+ contentValues);
+ }
+
+ private void assertMovedRawContact(long rawContactId, AccountWithDataSet account,
+ boolean isStarred) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(RawContacts._ID, rawContactId);
+ contentValues.put(RawContacts.DELETED, 0);
+ contentValues.put(RawContacts.STARRED, isStarred ? 1 : 0);
+ contentValues.putNull(RawContacts.SOURCE_ID);
+ contentValues.put(RawContacts.ACCOUNT_NAME, account.getAccountName());
+ contentValues.put(RawContacts.ACCOUNT_TYPE, account.getAccountType());
+ contentValues.put(RawContacts.DIRTY, 1);
+ assertStoredValues(RawContacts.CONTENT_URI,
+ RawContacts._ID + " = ?",
+ new String[]{String.valueOf(rawContactId)},
+ contentValues);
+ }
+
+ private void assertMoveStubExists(long rawContactId, String sourceId,
+ AccountWithDataSet account) {
+ assertEquals(1, getCount(RawContacts.CONTENT_URI,
+ RawContacts._ID + " <> ? and " + RawContacts.SOURCE_ID + " = ? and "
+ + RawContacts.DELETED + " = 1 and " + RawContacts.ACCOUNT_NAME + " = ? and "
+ + RawContacts.ACCOUNT_TYPE + " = ? and " + RawContacts.DIRTY + " = 1",
+ new String[] {
+ Long.toString(rawContactId),
+ sourceId,
+ account.getAccountName(),
+ account.getAccountType()
+ }));
+ }
+
+ private void assertMoveStubDoesNotExist(long rawContactId, AccountWithDataSet account) {
+ assertEquals(0, getCount(RawContacts.CONTENT_URI,
+ RawContacts._ID + " <> ? and "
+ + RawContacts.DELETED + " = 1 and " + RawContacts.ACCOUNT_NAME + " = ? and "
+ + RawContacts.ACCOUNT_TYPE + " = ?",
+ new String[] {
+ Long.toString(rawContactId),
+ account.getAccountName(),
+ account.getAccountType()
+ }));
+ }
+
+ private long createStarredRawContactForMove(String firstName, String lastName, String sourceId,
+ Account account) {
+ long rawContactId = RawContactUtil.createRawContactWithName(
+ mResolver, firstName, lastName, account);
+ ContentValues rawContactValues = new ContentValues();
+ rawContactValues.put(RawContacts.SOURCE_ID, sourceId);
+ rawContactValues.put(RawContacts.STARRED, 1);
+
+ if (account == null) {
+ rawContactValues.putNull(RawContacts.ACCOUNT_NAME);
+ rawContactValues.putNull(RawContacts.ACCOUNT_TYPE);
+ } else {
+ rawContactValues.put(RawContacts.ACCOUNT_NAME, account.name);
+ rawContactValues.put(RawContacts.ACCOUNT_TYPE, account.type);
+ }
+
+ RawContactUtil.update(mResolver, rawContactId, rawContactValues);
+ return rawContactId;
+ }
+
+ private void insertNonPortableData(
+ ContentResolver resolver, long rawContactId, String data1) {
+ ContentValues values = new ContentValues();
+ values.put(Data.DATA1, data1);
+ values.put(Data.MIMETYPE, NON_PORTABLE_MIMETYPE);
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ resolver.insert(Data.CONTENT_URI, values);
+ }
+
+ private void assertData(long rawContactId, String mimetype, String data1, int expectedCount) {
+ assertEquals(expectedCount, getCount(Data.CONTENT_URI,
+ Data.RAW_CONTACT_ID + " == ? AND "
+ + Data.MIMETYPE + " = ? AND "
+ + Data.DATA1 + " = ?",
+ new String[] {
+ Long.toString(rawContactId),
+ mimetype,
+ data1
+ }));
+ }
+
+ private void assertDataExists(long rawContactId, String mimetype, String data1) {
+ assertData(rawContactId, mimetype, data1, 1);
+ }
+
+ private void assertDataDoesNotExist(long rawContactId, String mimetype, String data1) {
+ assertData(rawContactId, mimetype, data1, 0);
+ }
+
+ private Long createGroupWithMembers(AccountWithDataSet account,
+ String title, String titleRes, List<Long> memberIds) {
+ ContentValues values = new ContentValues();
+ values.put(Groups.TITLE, title);
+ values.put(Groups.TITLE_RES, titleRes);
+ values.put(Groups.RES_PACKAGE, RES_PACKAGE);
+ values.put(Groups.ACCOUNT_NAME, account.getAccountName());
+ values.put(Groups.ACCOUNT_TYPE, account.getAccountType());
+ values.put(Groups.DATA_SET, account.getDataSet());
+ mResolver.insert(Groups.CONTENT_URI, values);
+ Long groupId = getGroupWithName(account, title, titleRes);
+
+ for (Long rawContactId: memberIds) {
+ values = new ContentValues();
+ values.put(GroupMembership.GROUP_ROW_ID, groupId);
+ values.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+ return groupId;
+ }
+
+ private void promoteToSystemGroup(Long groupId, String systemId, boolean isReadOnly) {
+ ContentValues values = new ContentValues();
+ values.put(Groups.SYSTEM_ID, systemId);
+ values.put(Groups.GROUP_IS_READ_ONLY, isReadOnly ? 1 : 0);
+ mResolver.update(Groups.CONTENT_URI, values,
+ Groups._ID + " = ?",
+ new String[]{
+ groupId.toString()
+ });
+ }
+
+ private void setGroupSourceId(Long groupId, String sourceId) {
+ ContentValues values = new ContentValues();
+ values.put(Groups.SOURCE_ID, sourceId);
+ mResolver.update(Groups.CONTENT_URI, values,
+ Groups._ID + " = ?",
+ new String[]{
+ groupId.toString()
+ });
+ }
+
+ private void assertInGroup(Long rawContactId, Long groupId) {
+ assertEquals(1, getCount(Data.CONTENT_URI,
+ GroupMembership.GROUP_ROW_ID + " == ? AND "
+ + Data.MIMETYPE + " = ? AND "
+ + GroupMembership.RAW_CONTACT_ID + " = ?",
+ new String[] {
+ Long.toString(groupId),
+ GroupMembership.CONTENT_ITEM_TYPE,
+ Long.toString(rawContactId)
+ }));
+ }
+
+ private void assertGroupState(Long groupId, AccountWithDataSet account, Set<Long> members,
+ boolean isDeleted) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Groups._ID, groupId);
+ contentValues.put(Groups.DELETED, isDeleted ? 1 : 0);
+ contentValues.put(Groups.ACCOUNT_NAME, account.getAccountName());
+ contentValues.put(Groups.ACCOUNT_TYPE, account.getAccountType());
+ contentValues.put(Groups.RES_PACKAGE, RES_PACKAGE);
+ contentValues.put(Groups.DIRTY, 1);
+ assertStoredValues(Groups.CONTENT_URI,
+ Groups._ID + " = ?",
+ new String[]{String.valueOf(groupId)},
+ contentValues);
+
+ assertEquals(members.size(), getCount(Data.CONTENT_URI,
+ GroupMembership.GROUP_ROW_ID + " == ? AND "
+ + Data.MIMETYPE + " = ?",
+ new String[] {
+ Long.toString(groupId),
+ GroupMembership.CONTENT_ITEM_TYPE
+ }));
+
+ for (Long member: members) {
+ assertInGroup(member, groupId);
+ }
+ }
+
+ private void assertGroup(Long groupId, AccountWithDataSet account, Set<Long> members) {
+ assertGroupState(groupId, account, members, /* isDeleted= */ false);
+ }
+
+ private void assertGroupDeleted(Long groupId, AccountWithDataSet account) {
+ assertGroupState(groupId, account, Set.of(), /* isDeleted= */ true);
+ }
+
+ private void assertGroupMoveStubExists(long groupId, String sourceId,
+ AccountWithDataSet account) {
+ assertEquals(1, getCount(Groups.CONTENT_URI,
+ Groups._ID + " <> ? and " + Groups.SOURCE_ID + " = ? and "
+ + Groups.DELETED + " = 1 and " + Groups.ACCOUNT_NAME + " = ? and "
+ + Groups.ACCOUNT_TYPE + " = ? and " + Groups.DIRTY + " = 1",
+ new String[] {
+ Long.toString(groupId),
+ sourceId,
+ account.getAccountName(),
+ account.getAccountType()
+ }));
+ }
+
+ private Long getGroupWithName(AccountWithDataSet account, String title, String titleRes) {
+ try (Cursor c = mResolver.query(Groups.CONTENT_URI,
+ new String[] { Groups._ID, },
+ Groups.ACCOUNT_NAME + " = ? AND "
+ + Groups.ACCOUNT_TYPE + " = ? AND "
+ + Groups.TITLE + " = ? AND "
+ + Groups.TITLE_RES + " = ?",
+ new String[] {
+ account.getAccountName(),
+ account.getAccountType(),
+ title,
+ titleRes
+ },
+ null)) {
+ assertNotNull(c);
+ c.moveToFirst();
+ return c.getLong(0);
+ }
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveDuplicateRawContacts() {
+ // create a duplicate pair of contacts
+ long sourceDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ DEST_ACCOUNT);
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // verify the duplicate raw contact in dest has been deleted in place
+ assertMovedContactIsDeleted(sourceDupeRawContactId, mSource);
+
+ // verify the duplicate destination contact is unaffected
+ assertMovedRawContact(destDupeRawContactId, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveUniqueRawContactsWithDataRows() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destRawContactId1 = RawContactUtil.createRawContactWithName(mResolver, DEST_ACCOUNT);
+ long destRawContactId2 = RawContactUtil.createRawContactWithName(mResolver, DEST_ACCOUNT);
+ // create a combination of data rows
+ DataUtil.insertStructuredName(mResolver, sourceRawContactId, "firstA", "lastA");
+ DataUtil.insertStructuredName(mResolver, sourceRawContactId, "firstB", "lastB");
+ DataUtil.insertStructuredName(mResolver, destRawContactId1, "firstA", "lastA");
+ DataUtil.insertStructuredName(mResolver, destRawContactId2, "firstB", "lastB");
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // Verify no stub was written since no source ID existed
+ assertMoveStubDoesNotExist(sourceRawContactId, mSource);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destRawContactId1, mDest, false);
+ assertMovedRawContact(destRawContactId2, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContacts() {
+ // create a near duplicate in the destination account
+ long destContactId = RawContactUtil.createRawContactWithName(
+ mResolver, "Foo", "Bar", DEST_ACCOUNT);
+
+ // create a near duplicate, unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, SOURCE_ACCOUNT);
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mDest, true);
+
+ // verify a stub has been written for the unique raw contact in the source account
+ assertMoveStubExists(uniqueContactId, SOURCE_ID, mSource);
+
+ // verify the original near duplicate contact remains unchanged (still not starred)
+ assertMovedRawContact(destContactId, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveUniqueRawContactsStubDisabled() {
+ // create a near duplicate in the destination account
+ long destContactId = RawContactUtil.createRawContactWithName(
+ mResolver, "Foo", "Bar", DEST_ACCOUNT);
+
+ // create a near duplicate, unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, SOURCE_ACCOUNT);
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mDest, true);
+
+ // verify a stub has been written for the unique raw contact in the source account
+ assertMoveStubDoesNotExist(uniqueContactId, mSource);
+
+ // verify no stub was created (since we've disabled stub creation)
+ assertMovedRawContact(destContactId, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactsFromNullAccount() {
+ mActor.setAccounts(new Account[]{DEST_ACCOUNT});
+ AccountWithDataSet source =
+ AccountWithDataSet.get(null, null, null);
+
+ // create a near duplicate in the destination account
+ long destContactId = RawContactUtil.createRawContactWithName(
+ mResolver, "Foo", "Bar", DEST_ACCOUNT);
+
+ // create a near duplicate, unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, /* account= */ null);
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(source), mDest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mDest, true);
+
+ // verify we didn't write a stub since null accounts don't need them (they're not synced)
+ assertEquals(0, getCount(RawContacts.CONTENT_URI,
+ RawContacts._ID + " <> ? and " + RawContacts.SOURCE_ID + " = ? and "
+ + RawContacts.DELETED + " = 1 and " + RawContacts.ACCOUNT_NAME + " IS NULL"
+ + " and " + RawContacts.ACCOUNT_TYPE + " IS NULL",
+ new String[] {
+ Long.toString(uniqueContactId),
+ SOURCE_ID
+ }));
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destContactId, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactsFromNullAccountToEmptyDestination() {
+ mActor.setAccounts(new Account[]{DEST_ACCOUNT});
+ AccountWithDataSet source =
+ AccountWithDataSet.get(null, null, null);
+
+ // create a unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, /* account= */ null);
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(source), mDest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mDest, true);
+
+ // verify we didn't write a stub since null accounts don't need them (they're not synced)
+ assertEquals(0, getCount(RawContacts.CONTENT_URI,
+ RawContacts._ID + " <> ? and " + RawContacts.SOURCE_ID + " = ? and "
+ + RawContacts.DELETED + " = 1 and " + RawContacts.ACCOUNT_NAME + " IS NULL"
+ + " and " + RawContacts.ACCOUNT_TYPE + " IS NULL",
+ new String[] {
+ Long.toString(uniqueContactId),
+ SOURCE_ID
+ }));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactsToNullAccount() {
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT});
+ AccountWithDataSet dest =
+ AccountWithDataSet.get(null, null, null);
+
+ // create a unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, SOURCE_ACCOUNT);
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), dest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, dest, true);
+
+ // verify a stub has been written for the unique raw contact in the source account
+ assertMoveStubExists(uniqueContactId, SOURCE_ID, mSource);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveUniqueRawContactsToNullAccountStubDisabled() {
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT});
+ AccountWithDataSet dest =
+ AccountWithDataSet.get(null, null, null);
+
+ // create a unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", SOURCE_ID, SOURCE_ACCOUNT);
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), dest);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, dest, true);
+
+ // verify no stub was created (since stub creation is disabled)
+ assertMoveStubDoesNotExist(uniqueContactId, mSource);
+ }
+
+ /**
+ * Move a contact between source and dest where both account have different account types.
+ * The contact is unique because of a non-portable data row, because the account types don't
+ * match, the non-portable data row will be deleted before matching the contacts and the contact
+ * will be deleted as a duplicate.
+ */
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactWithNonPortableDataRows() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destRawContactId = RawContactUtil.createRawContactWithName(mResolver, DEST_ACCOUNT);
+ // create a combination of data rows
+ DataUtil.insertStructuredName(mResolver, sourceRawContactId, "firstA", "lastA");
+ insertNonPortableData(mResolver, sourceRawContactId, "foo");
+ DataUtil.insertStructuredName(mResolver, destRawContactId, "firstA", "lastA");
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // Verify no stub was written since no source ID existed
+ assertMoveStubDoesNotExist(sourceRawContactId, mSource);
+
+ // verify the unique raw contact has been deleted as a duplicate
+ assertMovedContactIsDeleted(sourceRawContactId, mSource);
+ assertDataDoesNotExist(sourceRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ assertDataDoesNotExist(
+ sourceRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destRawContactId, mDest, false);
+ // the non portable data should still not exist on the destination account
+ assertDataDoesNotExist(destRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ // the existing data row in the destination account should be unaffected
+ assertDataExists(destRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+ }
+
+ /**
+ * Moves a contact between source and dest where both accounts have the same account type.
+ * The contact is unique because of a non-portable data row. Because the account types match,
+ * the non-portable data row will be considered while matching the contacts and the contact will
+ * be treated as unique.
+ */
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactsWithNonPortableDataRowsAccountTypesMatch() {
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT, DEST_ACCOUNT_WITH_SOURCE_TYPE});
+ AccountWithDataSet dest =
+ AccountWithDataSet.get(DEST_ACCOUNT_WITH_SOURCE_TYPE.name,
+ DEST_ACCOUNT_WITH_SOURCE_TYPE.type, null);
+
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ DEST_ACCOUNT_WITH_SOURCE_TYPE);
+ // create a combination of data rows
+ DataUtil.insertStructuredName(mResolver, sourceRawContactId, "firstA", "lastA");
+ insertNonPortableData(mResolver, sourceRawContactId, "foo");
+ DataUtil.insertStructuredName(mResolver, destRawContactId, "firstA", "lastA");
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), dest);
+
+ // Verify no stub was written since no source ID existed
+ assertMoveStubDoesNotExist(sourceRawContactId, mSource);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(sourceRawContactId, dest, false);
+ // all data rows should have moved with the source
+ assertDataExists(sourceRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ assertDataExists(sourceRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destRawContactId, dest, false);
+ // the non portable data should still not exist on the destination account
+ assertDataDoesNotExist(destRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ // the existing data row in the destination account should be unaffected
+ assertDataExists(destRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+ }
+
+ /**
+ * Moves a contact between source and dest where both accounts have the same account type.
+ * The contact is unique because of a non-portable data row. Because the account types match,
+ * the non-portable data row will be considered while matching the contacts and the contact will
+ * be treated as a duplicate.
+ */
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveDuplicateRawContactsWithNonPortableDataRowsAccountTypesMatch() {
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT, DEST_ACCOUNT_WITH_SOURCE_TYPE});
+ AccountWithDataSet dest =
+ AccountWithDataSet.get(DEST_ACCOUNT_WITH_SOURCE_TYPE.name,
+ DEST_ACCOUNT_WITH_SOURCE_TYPE.type, null);
+
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ DEST_ACCOUNT_WITH_SOURCE_TYPE);
+ // create a combination of data rows
+ DataUtil.insertStructuredName(mResolver, sourceRawContactId, "firstA", "lastA");
+ insertNonPortableData(mResolver, sourceRawContactId, "foo");
+ DataUtil.insertStructuredName(mResolver, destRawContactId, "firstA", "lastA");
+ insertNonPortableData(mResolver, destRawContactId, "foo");
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), dest);
+
+ // verify the duplicate contact has been deleted
+ assertMovedContactIsDeleted(sourceRawContactId, mSource);
+ assertDataDoesNotExist(sourceRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ assertDataDoesNotExist(
+ sourceRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destRawContactId, dest, false);
+ assertDataExists(destRawContactId, NON_PORTABLE_MIMETYPE, "foo");
+ assertDataExists(destRawContactId, StructuredName.CONTENT_ITEM_TYPE, "firstA lastA");
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveDuplicateNonSystemGroup() {
+ // create a duplicate pair of contacts
+ long sourceDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ DEST_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceDupeRawContactId));
+ setGroupSourceId(sourceGroup, SOURCE_ID);
+ long destGroup = createGroupWithMembers(mDest, "groupTitle",
+ "groupTitleRes", List.of(destDupeRawContactId));
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // verify the duplicate raw contact in dest has been deleted in place instead of creating
+ // a stub (because this is a duplicate non-system group, we delete in-place even if there's
+ // a source ID)
+ assertMovedContactIsDeleted(sourceDupeRawContactId, mSource);
+
+ // since sourceGroup was a duplicate of destGroup, it was deleted in place
+ assertGroupDeleted(sourceGroup, mSource);
+
+ // verify the duplicate destination contact is unaffected
+ assertMovedRawContact(destDupeRawContactId, mDest, false);
+ assertGroup(destGroup, mDest, Set.of(destDupeRawContactId));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueNonSystemGroup() {
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceRawContactId));
+
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // verify group and contact have been moved from the source account to the dest account
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+ assertGroup(sourceGroup, mDest, Set.of(sourceRawContactId));
+
+ // check that the only group in source got moved and no stub was written
+ assertEquals(0, getCount(Groups.CONTENT_URI,
+ Groups.ACCOUNT_NAME + " = ? AND "
+ + Groups.ACCOUNT_TYPE + " = ?",
+ new String[] {
+ mSource.getAccountName(),
+ mSource.getAccountType()
+ }));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueNonSystemGroupWithSourceId() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceRawContactId));
+ setGroupSourceId(sourceGroup, SOURCE_ID);
+
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // verify group and contact have been moved from the source account to the dest account
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+ assertGroup(sourceGroup, mDest, Set.of(sourceRawContactId));
+
+ // verify we created a move stub (since this was a unique non-system group with a source ID)
+ assertGroupMoveStubExists(sourceGroup, SOURCE_ID, mSource);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveUniqueNonSystemGroupWithSourceIdStubsDisabled() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceRawContactId));
+ setGroupSourceId(sourceGroup, SOURCE_ID);
+
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // verify group and contact have been moved from the source account to the dest account
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+ assertGroup(sourceGroup, mDest, Set.of(sourceRawContactId));
+
+ // check that the only group in source got moved and no stub was written (because we
+ // disabled stub creation)
+ assertEquals(0, getCount(Groups.CONTENT_URI,
+ Groups.ACCOUNT_NAME + " = ? AND "
+ + Groups.ACCOUNT_TYPE + " = ?",
+ new String[] {
+ mSource.getAccountName(),
+ mSource.getAccountType()
+ }));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG, Flags.FLAG_CP2_ACCOUNT_MOVE_SYNC_STUB_FLAG})
+ public void testMoveUniqueRawContactsWithGroups() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destRawContactId1 = RawContactUtil.createRawContactWithName(mResolver, DEST_ACCOUNT);
+ long destRawContactId2 = RawContactUtil.createRawContactWithName(mResolver, DEST_ACCOUNT);
+ // create a combination of data rows
+ long sourceGroup1 = createGroupWithMembers(
+ mSource, "group1Title", "group1TitleRes",
+ List.of(sourceRawContactId));
+ long sourceGroup2 = createGroupWithMembers(
+ mSource, "group2Title", "group2TitleRes",
+ List.of(sourceRawContactId));
+ promoteToSystemGroup(sourceGroup2, null, true);
+ long destGroup1 = createGroupWithMembers(
+ mDest, "group1Title", "group1TitleRes",
+ List.of(destRawContactId1));
+ long destGroup2 = createGroupWithMembers(
+ mDest, "group2Title", "group2TitleRes",
+ List.of(destRawContactId2));
+
+ // trigger the move
+ mMover.moveRawContactsWithSyncStubs(Set.of(mSource), mDest);
+
+ // Verify no stub was written since no source ID existed
+ assertMoveStubDoesNotExist(sourceRawContactId, mSource);
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+
+ // check the source contact got moved into the new group
+ assertGroup(destGroup1, mDest, Set.of(sourceRawContactId, destRawContactId1));
+ assertGroup(destGroup2, mDest, Set.of(sourceRawContactId, destRawContactId2));
+ assertGroupDeleted(sourceGroup1, mSource);
+ assertGroup(sourceGroup2, mSource, Set.of());
+
+ // verify the original near duplicate contact remains unchanged
+ assertMovedRawContact(destRawContactId1, mDest, false);
+ assertMovedRawContact(destRawContactId2, mDest, false);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveDuplicateSystemGroup() {
+ // create a duplicate pair of contacts
+ long sourceDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long destDupeRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ DEST_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceDupeRawContactId));
+ promoteToSystemGroup(sourceGroup, null, true);
+ setGroupSourceId(sourceGroup, SOURCE_ID);
+ long destGroup = createGroupWithMembers(mDest, "groupTitle",
+ "groupTitleRes", List.of(destDupeRawContactId));
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // verify the duplicate raw contact in dest has been deleted in place
+ assertMovedContactIsDeleted(sourceDupeRawContactId, mSource);
+
+ // Source group is a system group so it shouldn't get deleted
+ assertGroup(sourceGroup, mSource, Set.of());
+
+ // verify the duplicate destination contact is unaffected
+ assertMovedRawContact(destDupeRawContactId, mDest, false);
+
+ // The destination contact is the only one in destGroup since the source and destination
+ // contacts were true duplicates
+ assertGroup(destGroup, mDest, Set.of(destDupeRawContactId));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveUniqueSystemGroup() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceRawContactId));
+ promoteToSystemGroup(sourceGroup, null, true);
+ setGroupSourceId(sourceGroup, SOURCE_ID);
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // verify the duplicate raw contact in dest has been deleted in place
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+
+ // since sourceGroup is a system group, it cannot be deleted
+ assertGroup(sourceGroup, mSource, Set.of());
+
+ // verify that a copied group exists in dest now
+ long newGroup = getGroupWithName(mDest, "groupTitle", "groupTitleRes");
+ assertGroup(newGroup, mDest, Set.of(sourceRawContactId));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testDoNotMoveEmptyUniqueSystemGroup() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of());
+ promoteToSystemGroup(sourceGroup, null, true);
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // since sourceGroup is a system group, it cannot be deleted
+ assertGroup(sourceGroup, mSource, Set.of());
+
+ // verify the duplicate raw contact in dest has been deleted in place
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+
+ // check that we did not create a copy of the empty group in dest
+ assertEquals(0, getCount(Groups.CONTENT_URI,
+ Groups.ACCOUNT_NAME + " = ? AND "
+ + Groups.ACCOUNT_TYPE + " = ? AND "
+ + Groups.TITLE + " = ? AND "
+ + Groups.TITLE_RES + " = ?",
+ new String[] {
+ mDest.getAccountName(),
+ mDest.getAccountType(),
+ "groupTitle",
+ "groupTitleRes"
+ }));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testDoNotMoveAutoAddSystemGroup() {
+ // create a duplicate pair of contacts
+ long sourceRawContactId = RawContactUtil.createRawContactWithName(mResolver,
+ SOURCE_ACCOUNT);
+ long sourceGroup = createGroupWithMembers(mSource, "groupTitle",
+ "groupTitleRes", List.of(sourceRawContactId));
+ promoteToSystemGroup(sourceGroup, null, true);
+ ContentValues values = new ContentValues();
+ values.put(Groups.AUTO_ADD, 1);
+ mResolver.update(Groups.CONTENT_URI, values,
+ Groups._ID + " = ?",
+ new String[]{
+ Long.toString(sourceGroup)
+ });
+
+ // trigger the move
+ mMover.moveRawContacts(Set.of(mSource), mDest);
+
+ // since sourceGroup is a system group, it cannot be deleted
+ assertGroup(sourceGroup, mSource, Set.of());
+
+ // verify the duplicate raw contact in dest has been deleted in place
+ assertMovedRawContact(sourceRawContactId, mDest, false);
+
+ // check that we did not create a copy of the AUTO_ADD group in dest
+ assertEquals(0, getCount(Groups.CONTENT_URI,
+ Groups.ACCOUNT_NAME + " = ? AND "
+ + Groups.ACCOUNT_TYPE + " = ? AND "
+ + Groups.TITLE + " = ? AND "
+ + Groups.TITLE_RES + " = ?",
+ new String[] {
+ mDest.getAccountName(),
+ mDest.getAccountType(),
+ "groupTitle",
+ "groupTitleRes"
+ }));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveLocalToDefaultCloudAccount() {
+ mActor.setAccounts(new Account[]{DEST_CLOUD_ACCOUNT});
+ setDefaultAccountManagerAccounts(new Account[]{
+ DEST_CLOUD_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_CLOUD_ACCOUNT));
+
+ // create a unique contact in the (null/local) source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ null);
+
+ // trigger the move
+ mMover.moveLocalToCloudDefaultAccount();
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mCloudDest, true);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveToDefaultNonCloudAccount() {
+ mActor.setAccounts(new Account[]{DEST_ACCOUNT});
+ AccountWithDataSet source =
+ AccountWithDataSet.get(null, null, null);
+ setDefaultAccountManagerAccounts(new Account[]{
+ DEST_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_ACCOUNT));
+
+ // create a unique contact in the (null/local) source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ null);
+
+ // trigger the move
+ mMover.moveLocalToCloudDefaultAccount();
+
+ // verify the unique raw contact has *not* been moved
+ assertMovedRawContact(uniqueContactId, source, true);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveFromNonLocalAccount() {
+ mActor.setAccounts(new Account[]{SOURCE_ACCOUNT, DEST_CLOUD_ACCOUNT});
+ setDefaultAccountManagerAccounts(new Account[]{
+ SOURCE_ACCOUNT,
+ DEST_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_ACCOUNT));
+
+ // create a unique contact in the source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, SOURCE_ACCOUNT);
+
+ // trigger the move
+ mMover.moveLocalToCloudDefaultAccount();
+
+ // verify the unique raw contact has *not* been moved
+ assertMovedRawContact(uniqueContactId, mSource, true);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testMoveSimToDefaultCloudAccount() {
+ mActor.setAccounts(new Account[]{SIM_ACCOUNT, DEST_CLOUD_ACCOUNT});
+
+ setDefaultAccountManagerAccounts(new Account[]{
+ SIM_ACCOUNT,
+ DEST_CLOUD_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_CLOUD_ACCOUNT));
+
+ // create a unique contact in the (null/local) source account
+ long uniqueContactId = createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ SIM_ACCOUNT);
+
+ // trigger the move
+ mMover.moveSimToCloudDefaultAccount();
+
+ // verify the unique raw contact has been moved from the old -> new account
+ assertMovedRawContact(uniqueContactId, mCloudDest, true);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testGetNumberContactsWithSimContacts() {
+ mActor.setAccounts(new Account[]{SIM_ACCOUNT, DEST_CLOUD_ACCOUNT});
+
+ setDefaultAccountManagerAccounts(new Account[]{
+ SIM_ACCOUNT,
+ DEST_CLOUD_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_CLOUD_ACCOUNT));
+
+ // create a unique contact in a sim account
+ createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ SIM_ACCOUNT);
+ // create a unique contact in a non-sim account
+ createStarredRawContactForMove(
+ "Bar", "Baz", /* sourceId= */ null, /* account= */ DEST_CLOUD_ACCOUNT);
+
+ // get the counts
+ int localCount = mMover.getNumberLocalContacts();
+ int simCount = mMover.getNumberSimContacts();
+
+ // only contact is in the sim count
+ assertEquals(1, simCount);
+ assertEquals(0, localCount);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testGetNumberContactsWithLocalContacts() {
+ mActor.setAccounts(new Account[]{DEST_CLOUD_ACCOUNT});
+ setDefaultAccountManagerAccounts(new Account[]{
+ DEST_CLOUD_ACCOUNT,
+ });
+ mDefaultAccountManager.tryPushDefaultAccount(DefaultAccount.ofCloud(DEST_CLOUD_ACCOUNT));
+
+ // create a unique contact in the (null/local) source account
+ createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ null);
+
+ // trigger the move
+ int localCount = mMover.getNumberLocalContacts();
+ int simCount = mMover.getNumberSimContacts();
+
+ // only contact is in the local count
+ assertEquals(1, localCount);
+ assertEquals(0, simCount);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_CP2_ACCOUNT_MOVE_FLAG})
+ public void testGetNumberContactsWithoutCloudAccount() {
+ mActor.setAccounts(new Account[]{SIM_ACCOUNT});
+
+ setDefaultAccountManagerAccounts(new Account[]{SIM_ACCOUNT});
+ // create a unique contact in the sim and local source accounts
+ createStarredRawContactForMove(
+ "Foo", "Bar", /* sourceId= */ null, /* account= */ SIM_ACCOUNT);
+ createStarredRawContactForMove(
+ "Bar", "Baz", /* sourceId= */ null, /* account= */ null);
+
+ // trigger the move
+ int localCount = mMover.getNumberLocalContacts();
+ int simCount = mMover.getNumberSimContacts();
+
+ // no movable contacts without a Cloud Default Account
+ assertEquals(0, localCount);
+ assertEquals(0, simCount);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/UnSyncAccountsTest.java b/tests/src/com/android/providers/contacts/UnSyncAccountsTest.java
new file mode 100644
index 0000000..9d0a6b9
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/UnSyncAccountsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 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.accounts.Account;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StreamItemPhotos;
+import android.provider.ContactsContract.StreamItems;
+
+import androidx.test.filters.MediumTest;
+
+import com.android.providers.contacts.tests.R;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+/**
+ * Unit tests for {@link ContactsProvider2} UnSync API.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.UnSyncAccountsTest -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+@RunWith(JUnit4.class)
+public class UnSyncAccountsTest extends BaseContactsProvider2Test {
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testAccountUnSynced() {
+ Account readOnlyAccount = new Account("act", READ_ONLY_ACCOUNT_TYPE);
+ ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ mActor.setAccounts(new Account[]{readOnlyAccount, mAccount});
+ cp.onAccountsUpdated(new Account[]{readOnlyAccount, mAccount});
+
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ readOnlyAccount);
+ Uri photoUri1 = insertPhoto(rawContactId1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "john", "doe",
+ mAccount);
+ Uri photoUri2 = insertPhoto(rawContactId2);
+ storeValue(photoUri2, ContactsContract.CommonDataKinds.Photo.IS_SUPER_PRIMARY, "1");
+
+ assertAggregated(rawContactId1, rawContactId2);
+
+ long contactId = queryContactId(rawContactId1);
+
+ // The display name should come from the writable account
+ assertStoredValue(Uri.withAppendedPath(
+ ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI,
+ contactId),
+ ContactsContract.Contacts.Data.CONTENT_DIRECTORY),
+ ContactsContract.Contacts.DISPLAY_NAME, "john doe");
+
+ // The photo should be the one we marked as super-primary
+ assertStoredValue(ContactsContract.Contacts.CONTENT_URI, contactId,
+ ContactsContract.Contacts.PHOTO_ID, ContentUris.parseId(photoUri2));
+
+ mActor.setAccounts(new Account[]{readOnlyAccount, mAccount});
+ // Un Sync account.
+ cp.unSyncAccounts(new Account[]{mAccount});
+
+ // The display name should come from the remaining account
+ assertStoredValue(Uri.withAppendedPath(
+ ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI,
+ contactId),
+ ContactsContract.Contacts.Data.CONTENT_DIRECTORY),
+ ContactsContract.Contacts.DISPLAY_NAME, "John Doe");
+
+ // The photo should be the remaining one
+ assertStoredValue(ContactsContract.Contacts.CONTENT_URI, contactId,
+ ContactsContract.Contacts.PHOTO_ID, ContentUris.parseId(photoUri1));
+ }
+
+ @Test
+ public void testStreamItemsCleanedUpOnAccountUnSynced() {
+ Account doomedAccount = new Account("doom", "doom");
+ Account safeAccount = mAccount;
+ ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ mActor.setAccounts(new Account[]{doomedAccount, safeAccount});
+ cp.onAccountsUpdated(new Account[]{doomedAccount, safeAccount});
+
+ // Create a doomed raw contact, stream item, and photo.
+ long doomedRawContactId = RawContactUtil.createRawContactWithName(mResolver, doomedAccount);
+ Uri doomedStreamItemUri =
+ insertStreamItem(doomedRawContactId, buildGenericStreamItemValues(), doomedAccount);
+ long doomedStreamItemId = ContentUris.parseId(doomedStreamItemUri);
+ Uri doomedStreamItemPhotoUri = insertStreamItemPhoto(
+ doomedStreamItemId, buildGenericStreamItemPhotoValues(0), doomedAccount);
+
+ // Create a safe raw contact, stream item, and photo.
+ long safeRawContactId = RawContactUtil.createRawContactWithName(mResolver, safeAccount);
+ Uri safeStreamItemUri =
+ insertStreamItem(safeRawContactId, buildGenericStreamItemValues(), safeAccount);
+ long safeStreamItemId = ContentUris.parseId(safeStreamItemUri);
+ Uri safeStreamItemPhotoUri = insertStreamItemPhoto(
+ safeStreamItemId, buildGenericStreamItemPhotoValues(0), safeAccount);
+ long safeStreamItemPhotoId = ContentUris.parseId(safeStreamItemPhotoUri);
+
+ // UnSync the doomed account.
+ cp.unSyncAccounts(new Account[]{doomedAccount});
+
+ // Check that the doomed stuff has all been nuked.
+ ContentValues[] noValues = new ContentValues[0];
+ assertStoredValues(ContentUris.withAppendedId(RawContacts.CONTENT_URI, doomedRawContactId),
+ noValues);
+ assertStoredValues(doomedStreamItemUri, noValues);
+ assertStoredValues(doomedStreamItemPhotoUri, noValues);
+
+ // Check that the safe stuff lives on.
+ assertStoredValue(RawContacts.CONTENT_URI, safeRawContactId, RawContacts._ID,
+ safeRawContactId);
+ assertStoredValue(safeStreamItemUri, StreamItems._ID, safeStreamItemId);
+ assertStoredValue(safeStreamItemPhotoUri, StreamItemPhotos._ID, safeStreamItemPhotoId);
+ }
+
+ private ContentValues buildGenericStreamItemValues() {
+ ContentValues values = new ContentValues();
+ values.put(StreamItems.TEXT, "Hello world");
+ values.put(StreamItems.TIMESTAMP, System.currentTimeMillis());
+ values.put(StreamItems.COMMENTS, "Reshared by 123 others");
+ return values;
+ }
+
+ private ContentValues buildGenericStreamItemPhotoValues(int sortIndex) {
+ ContentValues values = new ContentValues();
+ values.put(StreamItemPhotos.SORT_INDEX, sortIndex);
+ values.put(StreamItemPhotos.PHOTO,
+ loadPhotoFromResource(R.drawable.earth_normal, PhotoSize.ORIGINAL));
+ return values;
+ }
+}