summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-12-06 02:23:45 +0000
committerRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-12-06 02:23:45 +0000
commit3f456157e56ddbdb7fff639c10f8d2afe77067f7 (patch)
treedb25b425622509a35dbf93a3df34523e1f208060
parenta42f690f2014b3588d67677c5f30859790600eb7 (diff)
parentde4adc8443ee5c16eac80e06c1e9e1ce9f0a2e8e (diff)
downloadandroid_packages_apps_ContactsCommon-3f456157e56ddbdb7fff639c10f8d2afe77067f7.tar.gz
android_packages_apps_ContactsCommon-3f456157e56ddbdb7fff639c10f8d2afe77067f7.tar.bz2
android_packages_apps_ContactsCommon-3f456157e56ddbdb7fff639c10f8d2afe77067f7.zip
Merge tag 'android-4.4.1_r1' into HEAD
Android 4.4.1 Release 1
-rw-r--r--TestCommon/Android.mk2
-rw-r--r--TestCommon/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java81
-rw-r--r--TestCommon/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java66
-rw-r--r--TestCommon/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java149
-rw-r--r--res/drawable-hdpi/list_longpressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--res/drawable-hdpi/list_pressed_holo_light.9.pngbin0 -> 159 bytes
-rw-r--r--res/drawable-mdpi/list_longpressed_holo_light.9.pngbin0 -> 155 bytes
-rw-r--r--res/drawable-mdpi/list_pressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--res/drawable-xhdpi/list_longpressed_holo_light.9.pngbin0 -> 162 bytes
-rw-r--r--res/drawable-xhdpi/list_pressed_holo_light.9.pngbin0 -> 163 bytes
-rw-r--r--res/drawable-xxhdpi/list_longpressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--res/drawable-xxhdpi/list_pressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--res/drawable/list_selector_background_transition_holo_light.xml20
-rw-r--r--res/values-af/strings.xml2
-rw-r--r--res/values-am/strings.xml4
-rw-r--r--res/values-ar/strings.xml80
-rw-r--r--res/values-be/strings.xml4
-rw-r--r--res/values-bg/strings.xml2
-rw-r--r--res/values-ca/strings.xml2
-rw-r--r--res/values-cs/strings.xml2
-rw-r--r--res/values-da/strings.xml2
-rw-r--r--res/values-de/strings.xml2
-rw-r--r--res/values-el/strings.xml2
-rw-r--r--res/values-en-rGB/strings.xml2
-rw-r--r--res/values-en-rIN/strings.xml2
-rw-r--r--res/values-es-rUS/strings.xml2
-rw-r--r--res/values-es/strings.xml2
-rw-r--r--res/values-et-rEE/strings.xml2
-rw-r--r--res/values-fa/strings.xml46
-rw-r--r--res/values-fi/strings.xml2
-rw-r--r--res/values-fr-rCA/strings.xml2
-rw-r--r--res/values-fr/strings.xml2
-rw-r--r--res/values-hi/strings.xml4
-rw-r--r--res/values-hr/strings.xml2
-rw-r--r--res/values-hu/strings.xml2
-rw-r--r--res/values-hy-rAM/strings.xml4
-rw-r--r--res/values-in/strings.xml2
-rw-r--r--res/values-it/strings.xml2
-rw-r--r--res/values-iw/strings.xml82
-rw-r--r--res/values-ja/strings.xml2
-rw-r--r--res/values-ka-rGE/strings.xml2
-rw-r--r--res/values-km-rKH/strings.xml2
-rw-r--r--res/values-ko/strings.xml2
-rw-r--r--res/values-lo-rLA/strings.xml2
-rw-r--r--res/values-lt/strings.xml2
-rw-r--r--res/values-lv/strings.xml2
-rw-r--r--res/values-mn-rMN/strings.xml2
-rw-r--r--res/values-ms-rMY/strings.xml2
-rw-r--r--res/values-nb/strings.xml2
-rw-r--r--res/values-nl/strings.xml2
-rw-r--r--res/values-pl/strings.xml2
-rw-r--r--res/values-pt-rPT/strings.xml2
-rw-r--r--res/values-pt/strings.xml2
-rw-r--r--res/values-rm/strings.xml4
-rw-r--r--res/values-ro/strings.xml2
-rw-r--r--res/values-ru/strings.xml2
-rw-r--r--res/values-sk/strings.xml2
-rw-r--r--res/values-sl/strings.xml2
-rw-r--r--res/values-sr/strings.xml2
-rw-r--r--res/values-sv/strings.xml2
-rw-r--r--res/values-sw/strings.xml2
-rw-r--r--res/values-th/strings.xml2
-rw-r--r--res/values-tl/strings.xml2
-rw-r--r--res/values-tr/strings.xml2
-rw-r--r--res/values-uk/strings.xml2
-rw-r--r--res/values-vi/strings.xml2
-rw-r--r--res/values-zh-rCN/strings.xml2
-rw-r--r--res/values-zh-rHK/strings.xml2
-rw-r--r--res/values-zh-rTW/strings.xml2
-rw-r--r--res/values-zu/strings.xml2
-rw-r--r--res/values/colors.xml2
-rw-r--r--res/values/strings.xml6
-rw-r--r--res/values/styles.xml10
-rw-r--r--src/com/android/contacts/common/ContactPhotoManager.java1
-rw-r--r--src/com/android/contacts/common/ContactsUtils.java144
-rw-r--r--src/com/android/contacts/common/GroupMetaData.java69
-rw-r--r--src/com/android/contacts/common/list/ContactEntryListAdapter.java7
-rw-r--r--src/com/android/contacts/common/list/ContactListItemView.java14
-rw-r--r--src/com/android/contacts/common/list/PhoneNumberPickerFragment.java3
-rw-r--r--src/com/android/contacts/common/list/PinnedHeaderListView.java11
-rw-r--r--src/com/android/contacts/common/model/Contact.java475
-rw-r--r--src/com/android/contacts/common/model/ContactLoader.java970
-rw-r--r--src/com/android/contacts/common/model/RawContact.java374
-rw-r--r--src/com/android/contacts/common/model/RawContactDelta.java556
-rw-r--r--src/com/android/contacts/common/model/RawContactDeltaList.java453
-rw-r--r--src/com/android/contacts/common/model/RawContactModifier.java1427
-rw-r--r--src/com/android/contacts/common/model/account/ExternalAccountType.java7
-rw-r--r--src/com/android/contacts/common/model/dataitem/DataItem.java162
-rw-r--r--src/com/android/contacts/common/model/dataitem/EmailDataItem.java48
-rw-r--r--src/com/android/contacts/common/model/dataitem/EventDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/IdentityDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/ImDataItem.java83
-rw-r--r--src/com/android/contacts/common/model/dataitem/NicknameDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/NoteDataItem.java36
-rw-r--r--src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java68
-rw-r--r--src/com/android/contacts/common/model/dataitem/PhoneDataItem.java80
-rw-r--r--src/com/android/contacts/common/model/dataitem/PhotoDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/RelationDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java40
-rw-r--r--src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java98
-rw-r--r--src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java68
-rw-r--r--src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java40
-rw-r--r--src/com/android/contacts/common/test/InjectedServices.java71
-rw-r--r--src/com/android/contacts/common/util/CommonDateUtils.java5
-rw-r--r--src/com/android/contacts/common/util/ContactLoaderUtils.java78
-rw-r--r--src/com/android/contacts/common/util/DataStatus.java165
-rw-r--r--src/com/android/contacts/common/util/DateUtils.java271
-rw-r--r--src/com/android/contacts/common/util/NameConverter.java236
-rw-r--r--src/com/android/contacts/common/util/UriUtils.java2
-rw-r--r--tests/Android.mk1
-rw-r--r--tests/src/com/android/contacts/common/ContactsUtilsTests.java69
-rw-r--r--tests/src/com/android/contacts/common/RawContactDeltaListTests.java593
-rw-r--r--tests/src/com/android/contacts/common/RawContactDeltaTests.java369
-rw-r--r--tests/src/com/android/contacts/common/RawContactModifierTests.java1235
-rw-r--r--tests/src/com/android/contacts/common/model/ContactLoaderTest.java388
-rw-r--r--tests/src/com/android/contacts/common/model/RawContactTest.java119
-rw-r--r--tests/src/com/android/contacts/common/util/NameConverterTests.java101
118 files changed, 9718 insertions, 121 deletions
diff --git a/TestCommon/Android.mk b/TestCommon/Android.mk
index 1f7f3611..c24a25b7 100644
--- a/TestCommon/Android.mk
+++ b/TestCommon/Android.mk
@@ -24,5 +24,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
# when running the unit tests.
LOCAL_JAVA_LIBRARIES := guava android.test.runner
+LOCAL_INSTRUMENTATION_FOR := com.android.contacts.common
+
LOCAL_MODULE := com.android.contacts.common.test
include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/TestCommon/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java b/TestCommon/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
new file mode 100644
index 00000000..8aed9891
--- /dev/null
+++ b/TestCommon/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.test.mocks;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A mock {@link AccountTypeManager} class.
+ */
+public class MockAccountTypeManager extends AccountTypeManager {
+
+ public AccountType[] mTypes;
+ public AccountWithDataSet[] mAccounts;
+
+ public MockAccountTypeManager(AccountType[] types, AccountWithDataSet[] accounts) {
+ this.mTypes = types;
+ this.mAccounts = accounts;
+ }
+
+ @Override
+ public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+ for (AccountType type : mTypes) {
+ if (Objects.equal(accountTypeWithDataSet.accountType, type.accountType)
+ && Objects.equal(accountTypeWithDataSet.dataSet, type.dataSet)) {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
+ return Arrays.asList(mAccounts);
+ }
+
+ @Override
+ public List<AccountWithDataSet> getGroupWritableAccounts() {
+ return Arrays.asList(mAccounts);
+ }
+
+ @Override
+ public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+ return Maps.newHashMap(); // Always returns empty
+ }
+
+ @Override
+ public List<AccountType> getAccountTypes(boolean writableOnly) {
+ final List<AccountType> ret = Lists.newArrayList();
+ synchronized (this) {
+ for (AccountType type : mTypes) {
+ if (!writableOnly || type.areContactsWritable()) {
+ ret.add(type);
+ }
+ }
+ }
+ return ret;
+ }
+}
diff --git a/TestCommon/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java b/TestCommon/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
new file mode 100644
index 00000000..13986264
--- /dev/null
+++ b/TestCommon/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2011 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.contacts.common.test.mocks;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.ImageView;
+
+import com.android.contacts.common.ContactPhotoManager;
+
+/**
+ * A photo preloader that always uses the "no contact" picture and never executes any real
+ * db queries
+ */
+public class MockContactPhotoManager extends ContactPhotoManager {
+ @Override
+ public void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
+ DefaultImageProvider defaultProvider) {
+ defaultProvider.applyDefaultImage(view, -1, darkTheme);
+ }
+
+ @Override
+ public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
+ DefaultImageProvider defaultProvider) {
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme);
+ }
+
+ @Override
+ public void removePhoto(ImageView view) {
+ view.setImageDrawable(null);
+ }
+
+ @Override
+ public void pause() {
+ }
+
+ @Override
+ public void resume() {
+ }
+
+ @Override
+ public void refreshCache() {
+ }
+
+ @Override
+ public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
+ }
+
+ @Override
+ public void preloadPhotosInBackground() {
+ }
+}
diff --git a/TestCommon/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java b/TestCommon/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
new file mode 100644
index 00000000..13d035ef
--- /dev/null
+++ b/TestCommon/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.test.mocks;
+
+import android.content.SharedPreferences;
+
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+
+ private HashMap<String, Object> mValues = Maps.newHashMap();
+ private HashMap<String, Object> mTempValues = Maps.newHashMap();
+
+ public Editor edit() {
+ return this;
+ }
+
+ public boolean contains(String key) {
+ return mValues.containsKey(key);
+ }
+
+ public Map<String, ?> getAll() {
+ return new HashMap<String, Object>(mValues);
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Boolean)mValues.get(key)).booleanValue();
+ }
+ return defValue;
+ }
+
+ public float getFloat(String key, float defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Float)mValues.get(key)).floatValue();
+ }
+ return defValue;
+ }
+
+ public int getInt(String key, int defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Integer)mValues.get(key)).intValue();
+ }
+ return defValue;
+ }
+
+ public long getLong(String key, long defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Long)mValues.get(key)).longValue();
+ }
+ return defValue;
+ }
+
+ public String getString(String key, String defValue) {
+ if (mValues.containsKey(key))
+ return (String)mValues.get(key);
+ return defValue;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ if (mValues.containsKey(key)) {
+ return (Set<String>) mValues.get(key);
+ }
+ return defValues;
+ }
+
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ mTempValues.put(key, Boolean.valueOf(value));
+ return this;
+ }
+
+ public Editor putFloat(String key, float value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putInt(String key, int value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putLong(String key, long value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putString(String key, String value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putStringSet(String key, Set<String> values) {
+ mTempValues.put(key, values);
+ return this;
+ }
+
+ public Editor remove(String key) {
+ mTempValues.remove(key);
+ return this;
+ }
+
+ public Editor clear() {
+ mTempValues.clear();
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public boolean commit() {
+ mValues = (HashMap<String, Object>)mTempValues.clone();
+ return true;
+ }
+
+ public void apply() {
+ commit();
+ }
+}
diff --git a/res/drawable-hdpi/list_longpressed_holo_light.9.png b/res/drawable-hdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 00000000..e9afcc92
--- /dev/null
+++ b/res/drawable-hdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_pressed_holo_light.9.png b/res/drawable-hdpi/list_pressed_holo_light.9.png
new file mode 100644
index 00000000..2054530e
--- /dev/null
+++ b/res/drawable-hdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_longpressed_holo_light.9.png b/res/drawable-mdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 00000000..3226ab76
--- /dev/null
+++ b/res/drawable-mdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_pressed_holo_light.9.png b/res/drawable-mdpi/list_pressed_holo_light.9.png
new file mode 100644
index 00000000..061904c4
--- /dev/null
+++ b/res/drawable-mdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/res/drawable-xhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 00000000..5532e88c
--- /dev/null
+++ b/res/drawable-xhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_pressed_holo_light.9.png b/res/drawable-xhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 00000000..f4af9265
--- /dev/null
+++ b/res/drawable-xhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 00000000..230d649b
--- /dev/null
+++ b/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/res/drawable-xxhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 00000000..1352a170
--- /dev/null
+++ b/res/drawable-xxhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable/list_selector_background_transition_holo_light.xml b/res/drawable/list_selector_background_transition_holo_light.xml
new file mode 100644
index 00000000..2541a2be
--- /dev/null
+++ b/res/drawable/list_selector_background_transition_holo_light.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<transition xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/list_pressed_holo_light" />
+ <item android:drawable="@drawable/list_longpressed_holo_light" />
+</transition>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 0cd72451..e1a2507a 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Voer in/uit"</string>
<string name="description_send_message" msgid="2630211225573754774">"Stuur boodskap aan <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Bel foon <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 88efbdee..3cea2c32 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -139,7 +139,7 @@
<string name="chat_gtalk" msgid="6043734883347741789">"Google Talkን በመጠቀም ይወያዩ"</string>
<string name="chat_icq" msgid="7538190395602030726">"ICQን በመጠቀም ይወያዩ"</string>
<string name="chat_jabber" msgid="4525546665986350869">"Jabberን በመጠቀም 271448"</string>
- <string name="chat" msgid="6297650784873558837">"ይወያዩ"</string>
+ <string name="chat" msgid="6297650784873558837">"ውይይት"</string>
<string name="description_minus_button" msgid="2142439445814730827">"ሰርዝ"</string>
<string name="expand_collapse_name_fields_description" msgid="5073419090665464541">"የስም መስኮችን ይዘርጉ ወይም ይሰብስቡ"</string>
<string name="list_filter_all_accounts" msgid="4265359896628915784">"ሁሉም እውቅያዎች"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"ያስመጡ/ወደ ውጪ ይላኩ"</string>
<string name="description_send_message" msgid="2630211225573754774">"ለ<xliff:g id="NAME">%1$s</xliff:g> መልዕክት ይላኩ"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"ወደ የ<xliff:g id="NAME">%1$s</xliff:g> ስልክ ይደውሉ"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"በ<xliff:g id="SOURCE">%1$s</xliff:g> በኩል"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> በ<xliff:g id="SOURCE">%2$s</xliff:g> በኩል"</string>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 0b873194..c85d371f 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -29,12 +29,12 @@
<string name="call_callback" msgid="687494744634190411">"الاتصال برقم معادوة الاتصال"</string>
<string name="call_car" msgid="9078083109758282133">"الاتصال بهاتف السيارة"</string>
<string name="call_company_main" msgid="8768047233970315359">"الاتصال بهاتف الشركة الرئيسي"</string>
- <string name="call_isdn" msgid="3342482563847537747">"اتصال بـ ISDN"</string>
+ <string name="call_isdn" msgid="3342482563847537747">"‏اتصال بـ ISDN"</string>
<string name="call_main" msgid="4640261071249938558">"الاتصال بالهاتف الرئيسي"</string>
<string name="call_other_fax" msgid="2536328553125487877">"الاتصال برقم الفاكس"</string>
<string name="call_radio" msgid="9055749313292712021">"اتصال بهاتف لاسلكي"</string>
<string name="call_telex" msgid="2909886619224843563">"الاتصال بالتلكس"</string>
- <string name="call_tty_tdd" msgid="6704940636171078852">"اتصال بـ TTY/TDD"</string>
+ <string name="call_tty_tdd" msgid="6704940636171078852">"‏اتصال بـ TTY/TDD"</string>
<string name="call_work_mobile" msgid="4408630608955148641">"الاتصال بجوال العمل"</string>
<string name="call_work_pager" msgid="3036932765279753793">"الاتصال بجهاز نداء العمل"</string>
<string name="call_assistant" msgid="5188990854852312696">"الاتصال بـ <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
@@ -50,12 +50,12 @@
<string name="sms_callback" msgid="728414485478941361">"إرسال رسالة نصية إلى هاتف معاودة الاتصال"</string>
<string name="sms_car" msgid="8543669230270954512">"إرسال رسالة نصية إلى هاتف السيارة"</string>
<string name="sms_company_main" msgid="5441902128445061862">"إرسال رسالة نصية إلى الهاتف الرئيسي للشركة"</string>
- <string name="sms_isdn" msgid="7142483128948042667">"إرسال رسالة نصية إلى ISDN"</string>
+ <string name="sms_isdn" msgid="7142483128948042667">"‏إرسال رسالة نصية إلى ISDN"</string>
<string name="sms_main" msgid="6815987840926184815">"إرسال رسالة نصية إلى الهاتف الرئيسي"</string>
<string name="sms_other_fax" msgid="8649822154879781466">"إرسال رسالة نصية إلى فاكس"</string>
<string name="sms_radio" msgid="8750339218480745940">"إرسال رسالة نصية إلى هاتف لاسلكي"</string>
<string name="sms_telex" msgid="5469333785775443999">"إرسال رسالة نصية إلى هاتف تلكس"</string>
- <string name="sms_tty_tdd" msgid="5057886576150816002">"إرسال رسالة نصية إلى TTY/TDD"</string>
+ <string name="sms_tty_tdd" msgid="5057886576150816002">"‏إرسال رسالة نصية إلى TTY/TDD"</string>
<string name="sms_work_mobile" msgid="8426872094268475047">"إرسال رسالة نصية إلى جوال العمل"</string>
<string name="sms_work_pager" msgid="499303540738557836">"إرسال رسالة نصية إلى جهاز نداء العمل"</string>
<string name="sms_assistant" msgid="2677586547377136683">"إرسال رسالة نصية إلى <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
@@ -131,14 +131,14 @@
<string name="map_work" msgid="9015981646907637207">"عرض عنوان العمل"</string>
<string name="map_other" msgid="55098598855607997">"عرض العنوان"</string>
<string name="map_custom" msgid="4943554530347163288">"عرض عنوان <xliff:g id="CUSTOM">%s</xliff:g>"</string>
- <string name="chat_aim" msgid="6479001490307452172">"الدردشة باستخدام AIM"</string>
- <string name="chat_msn" msgid="627481952525245054">"الدردشة باستخدام Windows Live"</string>
- <string name="chat_yahoo" msgid="5542489454092719897">"الدردشة باستخدام Yahoo"</string>
- <string name="chat_skype" msgid="3145166404699830256">"الدردشة باستخدام Skype"</string>
- <string name="chat_qq" msgid="2811762660890755082">"الدردشة باستخدام QQ"</string>
- <string name="chat_gtalk" msgid="6043734883347741789">"الدردشة باستخدام Google Talk"</string>
- <string name="chat_icq" msgid="7538190395602030726">"الدردشة باستخدام ICQ"</string>
- <string name="chat_jabber" msgid="4525546665986350869">"الدردشة باستخدام Jabber"</string>
+ <string name="chat_aim" msgid="6479001490307452172">"‏الدردشة باستخدام AIM"</string>
+ <string name="chat_msn" msgid="627481952525245054">"‏الدردشة باستخدام Windows Live"</string>
+ <string name="chat_yahoo" msgid="5542489454092719897">"‏الدردشة باستخدام Yahoo"</string>
+ <string name="chat_skype" msgid="3145166404699830256">"‏الدردشة باستخدام Skype"</string>
+ <string name="chat_qq" msgid="2811762660890755082">"‏الدردشة باستخدام QQ"</string>
+ <string name="chat_gtalk" msgid="6043734883347741789">"‏الدردشة باستخدام Google Talk"</string>
+ <string name="chat_icq" msgid="7538190395602030726">"‏الدردشة باستخدام ICQ"</string>
+ <string name="chat_jabber" msgid="4525546665986350869">"‏الدردشة باستخدام Jabber"</string>
<string name="chat" msgid="6297650784873558837">"دردشة"</string>
<string name="description_minus_button" msgid="2142439445814730827">"حذف"</string>
<string name="expand_collapse_name_fields_description" msgid="5073419090665464541">"توسيع أو تصغير حقول الاسم"</string>
@@ -159,25 +159,25 @@
<string name="listCustomView" msgid="1915154113477432033">"جهات الاتصال في عرض مخصص"</string>
<string name="listSingleContact" msgid="8525131203887307088">"جهة اتصال واحدة"</string>
<string name="dialog_new_contact_account" msgid="4107520273478326011">"إنشاء جهة اتصال ضمن حساب"</string>
- <string name="import_from_sim" msgid="4749894687871835873">"استيراد من بطاقة SIM"</string>
+ <string name="import_from_sim" msgid="4749894687871835873">"‏استيراد من بطاقة SIM"</string>
<string name="import_from_sdcard" product="default" msgid="6423964533801496764">"استيراد من وحدة التخزين"</string>
<string name="cancel_import_confirmation_message" msgid="7764915400478970495">"هل تريد إلغاء استيراد <xliff:g id="FILENAME">%s</xliff:g>؟"</string>
<string name="cancel_export_confirmation_message" msgid="4063783315931861656">"هل تريد إلغاء تصدير <xliff:g id="FILENAME">%s</xliff:g>؟"</string>
- <string name="cancel_vcard_import_or_export_failed" msgid="7450212880694781527">"تعذر إلغاء استيراد/تصدير vCard"</string>
+ <string name="cancel_vcard_import_or_export_failed" msgid="7450212880694781527">"‏تعذر إلغاء استيراد/تصدير vCard"</string>
<string name="fail_reason_unknown" msgid="8541352164960008557">"خطأ غير معروف."</string>
<string name="fail_reason_could_not_open_file" msgid="7041148341788958325">"تعذر فتح \"<xliff:g id="FILE_NAME">%s</xliff:g>\": <xliff:g id="EXACT_REASON">%s</xliff:g>\"."</string>
<string name="fail_reason_could_not_initialize_exporter" msgid="1231982631020480035">"تعذر بدء المُصدر: \"<xliff:g id="EXACT_REASON">%s</xliff:g>\"."</string>
<string name="fail_reason_no_exportable_contact" msgid="3717046989062541369">"ليست هناك جهة اتصال قابلة للتصدير."</string>
<string name="fail_reason_error_occurred_during_export" msgid="8426833603664168716">"حدث خطأ أثناء التصدير: \"<xliff:g id="EXACT_REASON">%s</xliff:g>\"."</string>
<string name="fail_reason_too_long_filename" msgid="3996610741248972232">"اسم الملف المطلوب أطول مما يجب (<xliff:g id="FILENAME">%s</xliff:g>)."</string>
- <string name="fail_reason_too_many_vcard" product="nosdcard" msgid="8720294715223591581">"هناك عدد أكبر مما يجب من ملفات vCard في وحدة التخزين."</string>
- <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"هناك ملفات vCard أكثر مما يجب على بطاقة SD."</string>
- <string name="fail_reason_io_error" msgid="7736686553669161933">"خطأ I/O"</string>
+ <string name="fail_reason_too_many_vcard" product="nosdcard" msgid="8720294715223591581">"‏هناك عدد أكبر مما يجب من ملفات vCard في وحدة التخزين."</string>
+ <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"‏هناك ملفات vCard أكثر مما يجب على بطاقة SD."</string>
+ <string name="fail_reason_io_error" msgid="7736686553669161933">"‏خطأ I/O"</string>
<string name="fail_reason_low_memory_during_import" msgid="3277485820827338116">"الذاكرة غير كافية. ربما يكون الملف أكبر مما يجب."</string>
- <string name="fail_reason_vcard_parse_error" msgid="514012644716565082">"تعذر تحليل vCard لسبب غير متوقع."</string>
+ <string name="fail_reason_vcard_parse_error" msgid="514012644716565082">"‏تعذر تحليل vCard لسبب غير متوقع."</string>
<string name="fail_reason_not_supported" msgid="388664373573337601">"التنسيق غير معتمد."</string>
- <string name="fail_reason_failed_to_collect_vcard_meta_info" msgid="3179066075701123895">"تعذر جمع معلومات وصفية حول ملفات vCard المحددة."</string>
- <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"تعذر استيراد ملف أو أكثر (%s)."</string>
+ <string name="fail_reason_failed_to_collect_vcard_meta_info" msgid="3179066075701123895">"‏تعذر جمع معلومات وصفية حول ملفات vCard المحددة."</string>
+ <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"‏تعذر استيراد ملف أو أكثر (%s)."</string>
<string name="exporting_vcard_finished_title" msgid="3581883972188707378">"تم الانتهاء من تصدير <xliff:g id="FILENAME">%s</xliff:g>."</string>
<string name="exporting_vcard_canceled_title" msgid="6993607802553630980">"تم إلغاء تصدير <xliff:g id="FILENAME">%s</xliff:g>."</string>
<string name="exporting_contact_list_title" msgid="5663945499580026953">"تصدير بيانات جهة الاتصال"</string>
@@ -185,39 +185,39 @@
<string name="composer_failed_to_get_database_infomation" msgid="7801276776746351694">"تعذر الحصول على معلومات قاعدة البيانات."</string>
<string name="composer_has_no_exportable_contact" product="tablet" msgid="1534625366506752907">"ليست هناك أية جهات اتصال قابلة للتصدير. إذا كانت لديك جهات اتصال على الجهاز اللوحي، فإن بعض موفري البيانات لا يسمحون بتصدير جهات الاتصال من الجهاز اللوحي."</string>
<string name="composer_has_no_exportable_contact" product="default" msgid="7063040740576745307">"ليست هناك أية جهات اتصال قابلة للتصدير. إذا كانت لديك جهات اتصال على هاتفك، فإن بعض موفري البيانات لا يسمحون بتصدير جهات الاتصال من الهاتف."</string>
- <string name="composer_not_initialized" msgid="6514403866246950877">"لم يبدأ مؤلف vCard بشكل صحيح."</string>
+ <string name="composer_not_initialized" msgid="6514403866246950877">"‏لم يبدأ مؤلف vCard بشكل صحيح."</string>
<string name="exporting_contact_failed_title" msgid="6059039606302373945">"تعذر التصدير"</string>
<string name="exporting_contact_failed_message" msgid="3922498776695333328">"لم يتم تصدير بيانات جهة الاتصال.\nالسبب: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\""</string>
<string name="no_sdcard_message" product="nosdcard" msgid="5131553549320038333">"لم يتم العثور على وحدة تخزين."</string>
- <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"لم يتم العثور على بطاقة SD."</string>
+ <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"‏لم يتم العثور على بطاقة SD."</string>
<string name="confirm_export_message" msgid="7234189779260525384">"سيتم تصدير قائمة جهات الاتصال إلى الملف: <xliff:g id="VCARD_FILENAME">%s</xliff:g>."</string>
<string name="importing_vcard_description" msgid="7206609625359484356">"جارٍ استيراد <xliff:g id="NAME">%s</xliff:g>"</string>
- <string name="reading_vcard_failed_title" msgid="4360989450476024034">"تعذرت قراءة بيانات vCard"</string>
- <string name="reading_vcard_canceled_title" msgid="1290217818311955708">"تم إلغاء قراءة بيانات vCard"</string>
- <string name="importing_vcard_finished_title" msgid="5590676758277628951">"تم الانتهاء من استيراد ملف vCard <xliff:g id="FILENAME">%s</xliff:g>"</string>
+ <string name="reading_vcard_failed_title" msgid="4360989450476024034">"‏تعذرت قراءة بيانات vCard"</string>
+ <string name="reading_vcard_canceled_title" msgid="1290217818311955708">"‏تم إلغاء قراءة بيانات vCard"</string>
+ <string name="importing_vcard_finished_title" msgid="5590676758277628951">"‏تم الانتهاء من استيراد ملف vCard <xliff:g id="FILENAME">%s</xliff:g>"</string>
<string name="importing_vcard_canceled_title" msgid="556913863250769870">"تم إلغاء استيراد <xliff:g id="FILENAME">%s</xliff:g>"</string>
<string name="vcard_import_will_start_message" msgid="7184603116300604514">"سيتم استيراد <xliff:g id="FILENAME">%s</xliff:g> بعد قليل."</string>
<string name="vcard_import_will_start_message_with_default_name" msgid="2560192057642180334">"سيتم استيراد الملف بعد قليل."</string>
- <string name="vcard_import_request_rejected_message" msgid="5209363425953891316">"تم رفض طلب استيراد vCard. أعد المحاولة لاحقًا."</string>
+ <string name="vcard_import_request_rejected_message" msgid="5209363425953891316">"‏تم رفض طلب استيراد vCard. أعد المحاولة لاحقًا."</string>
<string name="vcard_export_will_start_message" msgid="2168853666316526278">"سيتم تصدير <xliff:g id="FILENAME">%s</xliff:g> بعد قليل."</string>
- <string name="vcard_export_request_rejected_message" msgid="8044599716727705282">"تم رفض طلب تصدير vCard. أعد المحاولة لاحقًا."</string>
+ <string name="vcard_export_request_rejected_message" msgid="8044599716727705282">"‏تم رفض طلب تصدير vCard. أعد المحاولة لاحقًا."</string>
<string name="vcard_unknown_filename" msgid="4832657686149881554">"جهة اتصال"</string>
- <string name="caching_vcard_message" msgid="6635485116655518520">"يجري تخزين ملفات vCard مؤقتًا على وحدة تخزين مؤقتة محلية. سيبدأ الاستيراد الفعلي قريبًا."</string>
+ <string name="caching_vcard_message" msgid="6635485116655518520">"‏يجري تخزين ملفات vCard مؤقتًا على وحدة تخزين مؤقتة محلية. سيبدأ الاستيراد الفعلي قريبًا."</string>
<string name="percentage" msgid="6699653515463625488">"<xliff:g id="PERCENTAGE">%s</xliff:g><xliff:g id="PERCENTSIGN">%%</xliff:g>"</string>
- <string name="vcard_import_failed" msgid="4105296876768072508">"تعذر استيراد vCard."</string>
- <string name="import_failure_no_vcard_file" product="nosdcard" msgid="2066107150525521097">"لم يتم العثور على أي ملف vCard في وحدة التخزين."</string>
- <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"لم يتم العثور على ملف vCard على بطاقة SD."</string>
- <string name="nfc_vcard_file_name" msgid="305679412445157370">"استلام ج اتص.NFC"</string>
+ <string name="vcard_import_failed" msgid="4105296876768072508">"‏تعذر استيراد vCard."</string>
+ <string name="import_failure_no_vcard_file" product="nosdcard" msgid="2066107150525521097">"‏لم يتم العثور على أي ملف vCard في وحدة التخزين."</string>
+ <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"‏لم يتم العثور على ملف vCard على بطاقة SD."</string>
+ <string name="nfc_vcard_file_name" msgid="305679412445157370">"‏استلام ج اتص.NFC"</string>
<string name="confirm_export_title" msgid="3240899428149018226">"تصدير جهات الاتصال؟"</string>
- <string name="select_vcard_title" msgid="1536575036597557639">"اختيار ملف vCard"</string>
- <string name="import_one_vcard_string" msgid="6199149175802496361">"استيراد ملف vCard واحد"</string>
- <string name="import_multiple_vcard_string" msgid="8931879029943141122">"استيراد ملفات vCard متعددة"</string>
- <string name="import_all_vcard_string" msgid="1037495558362397535">"استيراد جميع ملفات vCard"</string>
- <string name="searching_vcard_message" product="nosdcard" msgid="2703499592557555234">"جارٍ البحث عن بيانات vCard في وحدة التخزين..."</string>
- <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"جارٍ البحث عن بيانات vCard على بطاقة SD..."</string>
+ <string name="select_vcard_title" msgid="1536575036597557639">"‏اختيار ملف vCard"</string>
+ <string name="import_one_vcard_string" msgid="6199149175802496361">"‏استيراد ملف vCard واحد"</string>
+ <string name="import_multiple_vcard_string" msgid="8931879029943141122">"‏استيراد ملفات vCard متعددة"</string>
+ <string name="import_all_vcard_string" msgid="1037495558362397535">"‏استيراد جميع ملفات vCard"</string>
+ <string name="searching_vcard_message" product="nosdcard" msgid="2703499592557555234">"‏جارٍ البحث عن بيانات vCard في وحدة التخزين..."</string>
+ <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"‏جارٍ البحث عن بيانات vCard على بطاقة SD..."</string>
<string name="caching_vcard_title" msgid="9185299351381102305">"تخزين مؤقت"</string>
<string name="scanning_sdcard_failed_message" product="nosdcard" msgid="8442457519490864500">"تعذر فحص وحدة التخزين. (السبب:\"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
- <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"تعذر فحص بطاقة SD. (السبب: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
+ <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"‏تعذر فحص بطاقة SD. (السبب: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
<string name="progress_notifier_message" msgid="359931715339778107">"جارٍ استيراد <xliff:g id="CURRENT_NUMBER">%s</xliff:g>/<xliff:g id="TOTAL_NUMBER">%s</xliff:g>: <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="export_to_sdcard" product="default" msgid="3665618085563543169">"تصدير إلى وحدة التخزين"</string>
<string name="share_visible_contacts" msgid="2150662668080757107">"مشاركة جهات الاتصال المرئية"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"استيراد/تصدير"</string>
<string name="description_send_message" msgid="2630211225573754774">"إرسال رسالة إلى <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"طلب هاتف <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"عبر <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> عبر <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 37abbed7..563cfbbf 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -240,4 +240,8 @@
<string name="menu_import_export" msgid="6446229463809981669">"Імпарт/экспарт"</string>
<string name="description_send_message" msgid="2630211225573754774">"Адправіць паведамленне карыстальніку: <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Набраць тэлефон карыстальніка <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <!-- no translation found for contact_status_update_attribution (8419168578670128134) -->
+ <skip />
+ <!-- no translation found for contact_status_update_attribution_with_date (7492465535645607473) -->
+ <skip />
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 4ecc9338..64e56dac 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Импортиране/Експортиране"</string>
<string name="description_send_message" msgid="2630211225573754774">"Изпращане на съобщение до <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Набиране на телефона на <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"чрез <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> чрез <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 5cc655d2..7abc16c8 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importa/exporta"</string>
<string name="description_send_message" msgid="2630211225573754774">"Envia un missatge a <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Marca el telèfon <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"mitjançant <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> mitjançant <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index b0a0cbf9..f9cf4a7c 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importovat/Exportovat"</string>
<string name="description_send_message" msgid="2630211225573754774">"Odeslat zprávu kontaktu <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Vytočit číslo kontaktu <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"prostřednictvím zdroje <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> prostřednictvím zdroje <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index ae0e0d1a..b125ef18 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importér/eksportér"</string>
<string name="description_send_message" msgid="2630211225573754774">"Send besked til <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Ring til telefon tilhørende <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index a1fb95cd..6e190361 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importieren/Exportieren"</string>
<string name="description_send_message" msgid="2630211225573754774">"Nachricht an <xliff:g id="NAME">%1$s</xliff:g> senden"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Telefonnummer für <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"über <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> über <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 12309865..7531b0c4 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Εισαγωγή/Εξαγωγή"</string>
<string name="description_send_message" msgid="2630211225573754774">"Αποστολή μηνύματος προς <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Κλήση αριθμού τηλεφώνου <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"μέσω <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> μέσω <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 5d6cdec3..4efb7367 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Import/export"</string>
<string name="description_send_message" msgid="2630211225573754774">"Send message to <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Dial phone <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 5d6cdec3..4efb7367 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Import/export"</string>
<string name="description_send_message" msgid="2630211225573754774">"Send message to <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Dial phone <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 14eba131..323a776a 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importar/exportar"</string>
<string name="description_send_message" msgid="2630211225573754774">"Enviar un mensaje a <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Marcar el teléfono de <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"a través de <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> a través de <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 2e9244ca..fa4b5aa4 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importar/exportar"</string>
<string name="description_send_message" msgid="2630211225573754774">"Enviar mensaje a <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Marcar teléfono <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"a través de <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> a través de <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml
index 9ce1a3a4..8e5a5f19 100644
--- a/res/values-et-rEE/strings.xml
+++ b/res/values-et-rEE/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Impordi/ekspordi"</string>
<string name="description_send_message" msgid="2630211225573754774">"Saada sõnum kontaktile <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Helista kontaktile <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"allika <xliff:g id="SOURCE">%1$s</xliff:g> kaudu"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> allika <xliff:g id="SOURCE">%2$s</xliff:g> kaudu"</string>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index a8e4f199..89fde069 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -29,16 +29,16 @@
<string name="call_callback" msgid="687494744634190411">"تماس با شماره بازگرداندن تماس"</string>
<string name="call_car" msgid="9078083109758282133">"تماس با تلفن خودرو"</string>
<string name="call_company_main" msgid="8768047233970315359">"تماس با خط اصلی شرکت"</string>
- <string name="call_isdn" msgid="3342482563847537747">"تماس با ISDN"</string>
+ <string name="call_isdn" msgid="3342482563847537747">"‏تماس با ISDN"</string>
<string name="call_main" msgid="4640261071249938558">"تماس با خط اصلی"</string>
<string name="call_other_fax" msgid="2536328553125487877">"تماس با نمابر"</string>
<string name="call_radio" msgid="9055749313292712021">"تماس با تلفن رادیویی"</string>
<string name="call_telex" msgid="2909886619224843563">"تماس با تلکس"</string>
- <string name="call_tty_tdd" msgid="6704940636171078852">"تماس با TTY/TDD"</string>
+ <string name="call_tty_tdd" msgid="6704940636171078852">"‏تماس با TTY/TDD"</string>
<string name="call_work_mobile" msgid="4408630608955148641">"تماس با تلفن همراه محل کار"</string>
<string name="call_work_pager" msgid="3036932765279753793">"تماس با پیجر محل کار"</string>
<string name="call_assistant" msgid="5188990854852312696">"تماس با <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
- <string name="call_mms" msgid="8998771923464696960">"تماس با MMS"</string>
+ <string name="call_mms" msgid="8998771923464696960">"‏تماس با MMS"</string>
<string name="sms_custom" msgid="4601748252470175865">"ارسال پیامک به <xliff:g id="CUSTOM">%s</xliff:g>"</string>
<string name="sms_home" msgid="7765831490534280540">"ارسال پیامک به تلفن منزل"</string>
<string name="sms_mobile" msgid="8387632124165893103">"ارسال پیامک به تلفن همراه"</string>
@@ -50,16 +50,16 @@
<string name="sms_callback" msgid="728414485478941361">"ارسال پیامک به شماره برگرداندن تماس"</string>
<string name="sms_car" msgid="8543669230270954512">"ارسال پیامک به تلفن خودرو"</string>
<string name="sms_company_main" msgid="5441902128445061862">"ارسال پیامک به خط اصلی شرکت"</string>
- <string name="sms_isdn" msgid="7142483128948042667">"ارسال پیامک به ISDN"</string>
+ <string name="sms_isdn" msgid="7142483128948042667">"‏ارسال پیامک به ISDN"</string>
<string name="sms_main" msgid="6815987840926184815">"ارسال پیامک به شماره اصلی"</string>
<string name="sms_other_fax" msgid="8649822154879781466">"ارسال پیامک به نمابر"</string>
<string name="sms_radio" msgid="8750339218480745940">"ارسال پیامک به تلفن رادیویی"</string>
<string name="sms_telex" msgid="5469333785775443999">"ارسال پیامک به تلکس"</string>
- <string name="sms_tty_tdd" msgid="5057886576150816002">"ارسال پیامک به TTY/TDD"</string>
+ <string name="sms_tty_tdd" msgid="5057886576150816002">"‏ارسال پیامک به TTY/TDD"</string>
<string name="sms_work_mobile" msgid="8426872094268475047">"ارسال پیامک به تلفن همراه محل کار"</string>
<string name="sms_work_pager" msgid="499303540738557836">"ارسال پیامک به پیجر محل کار"</string>
<string name="sms_assistant" msgid="2677586547377136683">"ارسال پیامک به <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
- <string name="sms_mms" msgid="4887206338311086612">"ارسال پیامک به MMS"</string>
+ <string name="sms_mms" msgid="4887206338311086612">"‏ارسال پیامک به MMS"</string>
<string name="clearFrequentsConfirmation_title" msgid="1482750234535491083">"لیست تماس مکرر پاک شود؟"</string>
<string name="clearFrequentsConfirmation" msgid="2101370440975269958">"با این کار شما در برنامه‌های «افراد» و «تلفن»، لیست افرادی را که با آن‌ها بیشترین تماس داشته‌اید پاک خواهید کرد و برنامه‌های ایمیل مجبور می‌شوند تنظیمات برگزیده آدرس‌دهی شما را از اول یاد بگیرند."</string>
<string name="clearFrequentsProgress_title" msgid="3738406170865048982">"در حال پاک کردن لیست تماس مکرر…"</string>
@@ -131,14 +131,14 @@
<string name="map_work" msgid="9015981646907637207">"مشاهده آدرس محل کار"</string>
<string name="map_other" msgid="55098598855607997">"مشاهده آدرس"</string>
<string name="map_custom" msgid="4943554530347163288">"مشاهده آدرس <xliff:g id="CUSTOM">%s</xliff:g>"</string>
- <string name="chat_aim" msgid="6479001490307452172">"گپ با استفاده از AIM"</string>
- <string name="chat_msn" msgid="627481952525245054">"گپ با استفاده از Windows Live"</string>
- <string name="chat_yahoo" msgid="5542489454092719897">"گپ با استفاده از Yahoo"</string>
- <string name="chat_skype" msgid="3145166404699830256">"گپ با استفاده از Skype"</string>
- <string name="chat_qq" msgid="2811762660890755082">"گپ با استفاده از QQ"</string>
- <string name="chat_gtalk" msgid="6043734883347741789">"گپ با استفاده از Google Talk"</string>
- <string name="chat_icq" msgid="7538190395602030726">"گپ با استفاده از ICQ"</string>
- <string name="chat_jabber" msgid="4525546665986350869">"گپ با استفاده از Jabber"</string>
+ <string name="chat_aim" msgid="6479001490307452172">"‏گپ با استفاده از AIM"</string>
+ <string name="chat_msn" msgid="627481952525245054">"‏گپ با استفاده از Windows Live"</string>
+ <string name="chat_yahoo" msgid="5542489454092719897">"‏گپ با استفاده از Yahoo"</string>
+ <string name="chat_skype" msgid="3145166404699830256">"‏گپ با استفاده از Skype"</string>
+ <string name="chat_qq" msgid="2811762660890755082">"‏گپ با استفاده از QQ"</string>
+ <string name="chat_gtalk" msgid="6043734883347741789">"‏گپ با استفاده از Google Talk"</string>
+ <string name="chat_icq" msgid="7538190395602030726">"‏گپ با استفاده از ICQ"</string>
+ <string name="chat_jabber" msgid="4525546665986350869">"‏گپ با استفاده از Jabber"</string>
<string name="chat" msgid="6297650784873558837">"گپ"</string>
<string name="description_minus_button" msgid="2142439445814730827">"حذف"</string>
<string name="expand_collapse_name_fields_description" msgid="5073419090665464541">"بزرگ یا کوچک کردن قسمت‌های نام"</string>
@@ -171,13 +171,13 @@
<string name="fail_reason_error_occurred_during_export" msgid="8426833603664168716">"خطایی در هنگام صادر کردن روی داد: \"<xliff:g id="EXACT_REASON">%s</xliff:g>\""</string>
<string name="fail_reason_too_long_filename" msgid="3996610741248972232">"نام فایل خیلی طولانی است (\"<xliff:g id="FILENAME">%s</xliff:g>\")."</string>
<string name="fail_reason_too_many_vcard" product="nosdcard" msgid="8720294715223591581">"تعداد فایل‌های کارت ویزیت در حافظه بسیار زیاد است."</string>
- <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"فایل‌های کارت ویزیت بسیار زیادی در کارت SD وجود دارد."</string>
+ <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"‏فایل‌های کارت ویزیت بسیار زیادی در کارت SD وجود دارد."</string>
<string name="fail_reason_io_error" msgid="7736686553669161933">"خطای ورودی/خروجی"</string>
<string name="fail_reason_low_memory_during_import" msgid="3277485820827338116">"حافظه کافی نیست. ممکن است فایل بسیار بزرگ باشد."</string>
<string name="fail_reason_vcard_parse_error" msgid="514012644716565082">"تفسیر کارت ویزیت به دلیل پیش‌بینی نشده‌ای ممکن نیست."</string>
<string name="fail_reason_not_supported" msgid="388664373573337601">"قالب پشتیبانی نمی‌شود."</string>
<string name="fail_reason_failed_to_collect_vcard_meta_info" msgid="3179066075701123895">"نمی‌توان اطلاعات متای فایل(های) کارت ویزیت داده شده را جمع‌آوری کرد."</string>
- <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"نمی‌توان یک یا چند فایل را وارد کرد (%s)."</string>
+ <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"‏نمی‌توان یک یا چند فایل را وارد کرد (%s)."</string>
<string name="exporting_vcard_finished_title" msgid="3581883972188707378">"صادر کردن <xliff:g id="FILENAME">%s</xliff:g> پایان یافت."</string>
<string name="exporting_vcard_canceled_title" msgid="6993607802553630980">"صادر کردن <xliff:g id="FILENAME">%s</xliff:g> لغو شد."</string>
<string name="exporting_contact_list_title" msgid="5663945499580026953">"صدور اطلاعات مخاطب"</string>
@@ -187,9 +187,9 @@
<string name="composer_has_no_exportable_contact" product="default" msgid="7063040740576745307">"هیچ مخاطب قابل صدوری وجود ندارد. اگر در گوشی خود مخاطبینی دارید، بعضی از ارائه‌دهندگان داده ممکن است اجازه ندهند تا مخاطبین از گوشی صادر شوند."</string>
<string name="composer_not_initialized" msgid="6514403866246950877">"سازنده فایل کارت ویزیت به درستی اجرا نشد."</string>
<string name="exporting_contact_failed_title" msgid="6059039606302373945">"صادر نمی‌شود"</string>
- <string name="exporting_contact_failed_message" msgid="3922498776695333328">"داده‎های مخاطب صادر نشد.\nعلت: «<xliff:g id="FAIL_REASON">%s</xliff:g>»"</string>
+ <string name="exporting_contact_failed_message" msgid="3922498776695333328">"‏داده‎های مخاطب صادر نشد.\nعلت: «<xliff:g id="FAIL_REASON">%s</xliff:g>»"</string>
<string name="no_sdcard_message" product="nosdcard" msgid="5131553549320038333">"هیچ دستگاه ذخیره‌ای یافت نشد."</string>
- <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"کارت SD یافت نشد."</string>
+ <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"‏کارت SD یافت نشد."</string>
<string name="confirm_export_message" msgid="7234189779260525384">"لیست مخاطبین شما به این فایل صادر می‌شود: <xliff:g id="VCARD_FILENAME">%s</xliff:g>."</string>
<string name="importing_vcard_description" msgid="7206609625359484356">"وارد کردن <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="reading_vcard_failed_title" msgid="4360989450476024034">"خواندن داده‌های کارت ویزیت ممکن نیست"</string>
@@ -206,18 +206,18 @@
<string name="percentage" msgid="6699653515463625488">"<xliff:g id="PERCENTAGE">%s</xliff:g><xliff:g id="PERCENTSIGN">%%</xliff:g>"</string>
<string name="vcard_import_failed" msgid="4105296876768072508">"وارد کردن کارت ویزیت انجام نشد."</string>
<string name="import_failure_no_vcard_file" product="nosdcard" msgid="2066107150525521097">"هیچ فایل کارت ویزیتی در حافظه یافت نشد."</string>
- <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"هیچ فایل کارت ویزیتی در کارت SD یافت نشد."</string>
- <string name="nfc_vcard_file_name" msgid="305679412445157370">"دریافت مخاطب باNFC"</string>
+ <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"‏هیچ فایل کارت ویزیتی در کارت SD یافت نشد."</string>
+ <string name="nfc_vcard_file_name" msgid="305679412445157370">"‏دریافت مخاطب باNFC"</string>
<string name="confirm_export_title" msgid="3240899428149018226">"مخاطبین صادر شوند؟"</string>
<string name="select_vcard_title" msgid="1536575036597557639">"انتخاب فایل کارت ویزیت"</string>
<string name="import_one_vcard_string" msgid="6199149175802496361">"وارد کردن یک فایل کارت ویزیت"</string>
<string name="import_multiple_vcard_string" msgid="8931879029943141122">"وارد کردن چند فایل کارت ویزیت"</string>
<string name="import_all_vcard_string" msgid="1037495558362397535">"وارد کردن همه فایل‌های کارت ویزیت"</string>
<string name="searching_vcard_message" product="nosdcard" msgid="2703499592557555234">"جستجوی داده کارت ویزیت در دستگاه ذخیره..."</string>
- <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"در حال جستجوی داده‌های کارت ویزیت در کارت SD..."</string>
+ <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"‏در حال جستجوی داده‌های کارت ویزیت در کارت SD..."</string>
<string name="caching_vcard_title" msgid="9185299351381102305">"در حال ذخیره در حافظهٔ پنهان"</string>
<string name="scanning_sdcard_failed_message" product="nosdcard" msgid="8442457519490864500">"دستگاه ذخیره نمی‌تواند بررسی شود. (علت: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
- <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"کارت SD نمی‌تواند بررسی شود. (علت: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
+ <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"‏کارت SD نمی‌تواند بررسی شود. (علت: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
<string name="progress_notifier_message" msgid="359931715339778107">"وارد کردن <xliff:g id="CURRENT_NUMBER">%s</xliff:g><xliff:g id="TOTAL_NUMBER">%s</xliff:g>: <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="export_to_sdcard" product="default" msgid="3665618085563543169">"صادر کردن به حافظه"</string>
<string name="share_visible_contacts" msgid="2150662668080757107">"اشتراک‌گذاری مخاطبین قابل مشاهده"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"وارد کردن/صادر کردن"</string>
<string name="description_send_message" msgid="2630211225573754774">"ارسال پیام به <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"شماره‌گیری شماره <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"از طریق <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> از طریق <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index c8481d9a..6ea6a29a 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Tuo/vie"</string>
<string name="description_send_message" msgid="2630211225573754774">"Lähetä viesti henkilölle <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Henkilön <xliff:g id="NAME">%1$s</xliff:g> puhelinnumero"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"lähteestä <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> lähteestä <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index c9950f35..b116ff7f 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importer/Exporter"</string>
<string name="description_send_message" msgid="2630211225573754774">"Envoyer un message à <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Composer le numéro de téléphone de <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"par <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> par <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 21974c06..418efcea 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importer/Exporter"</string>
<string name="description_send_message" msgid="2630211225573754774">"Envoyer un message à <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Composer le numéro de téléphone de <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index e83651fd..8f920174 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -61,7 +61,7 @@
<string name="sms_assistant" msgid="2677586547377136683">"<xliff:g id="ASSISTANT">%s</xliff:g> पर पाठ संदेश भेजें"</string>
<string name="sms_mms" msgid="4887206338311086612">"MMS पर पाठ संदेश भेजें"</string>
<string name="clearFrequentsConfirmation_title" msgid="1482750234535491083">"अक्‍सर किए जाने वाले संपर्क साफ करें?"</string>
- <string name="clearFrequentsConfirmation" msgid="2101370440975269958">"आपको लोग और फ़ोन एप्‍स में अक्‍सर संपर्क करने की सूची साफ़ करनी होगी, और अपने ईमेल एप्स को आपकी संबोधन प्राथमिकताओं को प्रारंभ से जानने के लिए बाध्‍य करना होगा."</string>
+ <string name="clearFrequentsConfirmation" msgid="2101370440975269958">"आपको लोग और फ़ोन ऐप्स में अक्‍सर संपर्क करने की सूची साफ़ करनी होगी, और अपने ईमेल ऐप्स को आपकी संबोधन प्राथमिकताओं को प्रारंभ से जानने के लिए बाध्‍य करना होगा."</string>
<string name="clearFrequentsProgress_title" msgid="3738406170865048982">"अक्सर किए जाने वाले संपर्क साफ कर रहा है…"</string>
<string name="status_available" msgid="4832569677396634846">"उपलब्ध"</string>
<string name="status_away" msgid="6267905184545881094">"दूर"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"आयात करें/निर्यात करें"</string>
<string name="description_send_message" msgid="2630211225573754774">"<xliff:g id="NAME">%1$s</xliff:g> को संदेश भेजें"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"फ़ोन <xliff:g id="NAME">%1$s</xliff:g> डायल करें"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"<xliff:g id="SOURCE">%1$s</xliff:g> द्वारा"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="SOURCE">%2$s</xliff:g> द्वारा <xliff:g id="DATE">%1$s</xliff:g> को"</string>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index b76f82ee..a7fa405f 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Uvoz/izvoz"</string>
<string name="description_send_message" msgid="2630211225573754774">"Slanje poruke kontaktu <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Biranje telefonskog broja za kontakt <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"putem izvora <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> putem izvora <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index cccc684f..17472d6a 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importálás/exportálás"</string>
<string name="description_send_message" msgid="2630211225573754774">"Üzenet küldése a következőnek: <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g> tárcsázása"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"itt: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> itt: <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml
index 0f1f94fe..eec95a73 100644
--- a/res/values-hy-rAM/strings.xml
+++ b/res/values-hy-rAM/strings.xml
@@ -225,7 +225,7 @@
<string name="dialog_import" msgid="5177004290082451296">"Ներմուծել կոնտակտներ"</string>
<string name="share_error" msgid="665756457151793108">"Հնարավոր չէ տարածել կոնտակտը:"</string>
<string name="menu_search" msgid="7464453023659824700">"Որոնել"</string>
- <string name="menu_contacts_filter" msgid="586356478145511794">"Ցուցադրելի կոնտկատներ"</string>
+ <string name="menu_contacts_filter" msgid="586356478145511794">"Ցուցադրվող կոնտկատներ"</string>
<string name="activity_title_contacts_filter" msgid="7689519428197855166">"Ցուցադրվող կոնտակտներ"</string>
<string name="custom_list_filter" msgid="582616656313514803">"Սահմանել հատուկ տեսքը"</string>
<string name="hint_findContacts" msgid="28151707326753522">"Գտնել կոնտակտներ"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Ներմուծել/արտահանել"</string>
<string name="description_send_message" msgid="2630211225573754774">"Ուղարկել հաղորդագրություն <xliff:g id="NAME">%1$s</xliff:g>-ին"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Հավաքագրել <xliff:g id="NAME">%1$s</xliff:g>-ի հեռախոսահամարը"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"<xliff:g id="SOURCE">%1$s</xliff:g>-ի միջոցով"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>` <xliff:g id="SOURCE">%2$s</xliff:g>-ի միջոցով"</string>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 9b45342e..a9611960 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Impor/ekspor"</string>
<string name="description_send_message" msgid="2630211225573754774">"Kirim pesan ke <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Telepon nomor <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"melalui <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> melalui <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 8fbb0b76..5736e5d7 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importa/esporta"</string>
<string name="description_send_message" msgid="2630211225573754774">"Invia messaggio a <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Componi numero di telefono <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"tramite <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> tramite <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 84b87e0a..eadc983f 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -29,16 +29,16 @@
<string name="call_callback" msgid="687494744634190411">"התקשר למספר להתקשרות חזרה"</string>
<string name="call_car" msgid="9078083109758282133">"התקשר למספר במכונית"</string>
<string name="call_company_main" msgid="8768047233970315359">"התקשר למספר הראשי של החברה"</string>
- <string name="call_isdn" msgid="3342482563847537747">"התקשר ל-ISDN"</string>
+ <string name="call_isdn" msgid="3342482563847537747">"‏התקשר ל-ISDN"</string>
<string name="call_main" msgid="4640261071249938558">"התקשר למספר הראשי"</string>
<string name="call_other_fax" msgid="2536328553125487877">"התקשר לפקס"</string>
<string name="call_radio" msgid="9055749313292712021">"התקשר לרדיו"</string>
<string name="call_telex" msgid="2909886619224843563">"התקשר לטלקס"</string>
- <string name="call_tty_tdd" msgid="6704940636171078852">"התקשר ל-TTY/TDD"</string>
+ <string name="call_tty_tdd" msgid="6704940636171078852">"‏התקשר ל-TTY/TDD"</string>
<string name="call_work_mobile" msgid="4408630608955148641">"התקשר לנייד של העבודה"</string>
<string name="call_work_pager" msgid="3036932765279753793">"התקשר לזימונית של העבודה"</string>
<string name="call_assistant" msgid="5188990854852312696">"התקשר אל <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
- <string name="call_mms" msgid="8998771923464696960">"התקשר ל-MMS"</string>
+ <string name="call_mms" msgid="8998771923464696960">"‏התקשר ל-MMS"</string>
<string name="sms_custom" msgid="4601748252470175865">"שלח הודעת טקסט אל <xliff:g id="CUSTOM">%s</xliff:g>"</string>
<string name="sms_home" msgid="7765831490534280540">"שלח הודעת טקסט לבית"</string>
<string name="sms_mobile" msgid="8387632124165893103">"שלח הודעת טקסט לנייד"</string>
@@ -50,16 +50,16 @@
<string name="sms_callback" msgid="728414485478941361">"שלח הודעת טקסט למספר להתקשרות חזרה"</string>
<string name="sms_car" msgid="8543669230270954512">"שלח הודעת טקסט למספר במכונית"</string>
<string name="sms_company_main" msgid="5441902128445061862">"שלח הודעת טקסט למספר הראשי של החברה"</string>
- <string name="sms_isdn" msgid="7142483128948042667">"שלח הודעת טקסט ל-ISDN"</string>
+ <string name="sms_isdn" msgid="7142483128948042667">"‏שלח הודעת טקסט ל-ISDN"</string>
<string name="sms_main" msgid="6815987840926184815">"שלח הודעת טקסט למספר הראשי"</string>
<string name="sms_other_fax" msgid="8649822154879781466">"שלח הודעת טקסט לפקס"</string>
<string name="sms_radio" msgid="8750339218480745940">"שלח הודעת טקסט לרדיו"</string>
<string name="sms_telex" msgid="5469333785775443999">"שלח הודעת טקסט לטלקס"</string>
- <string name="sms_tty_tdd" msgid="5057886576150816002">"שלח הודעת טקסט ל-TTY/TDD"</string>
+ <string name="sms_tty_tdd" msgid="5057886576150816002">"‏שלח הודעת טקסט ל-TTY/TDD"</string>
<string name="sms_work_mobile" msgid="8426872094268475047">"שלח הודעת טקסט לנייד של עבודה"</string>
<string name="sms_work_pager" msgid="499303540738557836">"שלח הודעת טקסט לזימונית של עבודה"</string>
<string name="sms_assistant" msgid="2677586547377136683">"שלח הודעת טקסט אל <xliff:g id="ASSISTANT">%s</xliff:g>"</string>
- <string name="sms_mms" msgid="4887206338311086612">"שלח הודעת טקסט ל-MMS"</string>
+ <string name="sms_mms" msgid="4887206338311086612">"‏שלח הודעת טקסט ל-MMS"</string>
<string name="clearFrequentsConfirmation_title" msgid="1482750234535491083">"האם למחוק אנשי קשר קבועים?"</string>
<string name="clearFrequentsConfirmation" msgid="2101370440975269958">"פעולה זו תמחק את רשימת אנשי הקשר הקבועים באפליקציות \'אנשים\' ו\'טלפון\' ותאלץ את יישומי הדוא\"ל ללמוד מהתחלה את העדפות הכתובות שלך."</string>
<string name="clearFrequentsProgress_title" msgid="3738406170865048982">"מוחק אנשי קשר קבועים…"</string>
@@ -131,14 +131,14 @@
<string name="map_work" msgid="9015981646907637207">"הצג כתובת עבודה"</string>
<string name="map_other" msgid="55098598855607997">"הצג כתובת"</string>
<string name="map_custom" msgid="4943554530347163288">"הצג כתובת <xliff:g id="CUSTOM">%s</xliff:g>"</string>
- <string name="chat_aim" msgid="6479001490307452172">"שוחח בצ\'אט באמצעות AIM"</string>
- <string name="chat_msn" msgid="627481952525245054">"שוחח בצ\'אט באמצעות Windows Live"</string>
- <string name="chat_yahoo" msgid="5542489454092719897">"שוחח בצ\'אט באמצעות Yahoo"</string>
- <string name="chat_skype" msgid="3145166404699830256">"שוחח בצ\'אט באמצעות Skype"</string>
- <string name="chat_qq" msgid="2811762660890755082">"שוחח בצ\'אט באמצעות QQ"</string>
- <string name="chat_gtalk" msgid="6043734883347741789">"שוחח בצ\'אט באמצעות Google Talk"</string>
- <string name="chat_icq" msgid="7538190395602030726">"שוחח בצ\'אט באמצעות ICQ"</string>
- <string name="chat_jabber" msgid="4525546665986350869">"שוחח בצ\'אט באמצעות Jabber"</string>
+ <string name="chat_aim" msgid="6479001490307452172">"‏שוחח בצ\'אט באמצעות AIM"</string>
+ <string name="chat_msn" msgid="627481952525245054">"‏שוחח בצ\'אט באמצעות Windows Live"</string>
+ <string name="chat_yahoo" msgid="5542489454092719897">"‏שוחח בצ\'אט באמצעות Yahoo"</string>
+ <string name="chat_skype" msgid="3145166404699830256">"‏שוחח בצ\'אט באמצעות Skype"</string>
+ <string name="chat_qq" msgid="2811762660890755082">"‏שוחח בצ\'אט באמצעות QQ"</string>
+ <string name="chat_gtalk" msgid="6043734883347741789">"‏שוחח בצ\'אט באמצעות Google Talk"</string>
+ <string name="chat_icq" msgid="7538190395602030726">"‏שוחח בצ\'אט באמצעות ICQ"</string>
+ <string name="chat_jabber" msgid="4525546665986350869">"‏שוחח בצ\'אט באמצעות Jabber"</string>
<string name="chat" msgid="6297650784873558837">"צ\'אט"</string>
<string name="description_minus_button" msgid="2142439445814730827">"מחק"</string>
<string name="expand_collapse_name_fields_description" msgid="5073419090665464541">"הרחב או כווץ שמות של שדות"</string>
@@ -159,25 +159,25 @@
<string name="listCustomView" msgid="1915154113477432033">"אנשי קשר בתצוגה מותאמת אישית"</string>
<string name="listSingleContact" msgid="8525131203887307088">"איש קשר יחיד"</string>
<string name="dialog_new_contact_account" msgid="4107520273478326011">"צור איש קשר בחשבון"</string>
- <string name="import_from_sim" msgid="4749894687871835873">"יבא מכרטיס SIM"</string>
+ <string name="import_from_sim" msgid="4749894687871835873">"‏יבא מכרטיס SIM"</string>
<string name="import_from_sdcard" product="default" msgid="6423964533801496764">"יבא מאמצעי אחסון"</string>
<string name="cancel_import_confirmation_message" msgid="7764915400478970495">"האם לבטל את הייבוא של <xliff:g id="FILENAME">%s</xliff:g>?"</string>
<string name="cancel_export_confirmation_message" msgid="4063783315931861656">"האם לבטל את הייצוא של <xliff:g id="FILENAME">%s</xliff:g>?"</string>
- <string name="cancel_vcard_import_or_export_failed" msgid="7450212880694781527">"לא ניתן היה לבטל ייבוא/ייצוא של vCard"</string>
+ <string name="cancel_vcard_import_or_export_failed" msgid="7450212880694781527">"‏לא ניתן היה לבטל ייבוא/ייצוא של vCard"</string>
<string name="fail_reason_unknown" msgid="8541352164960008557">"שגיאה לא ידועה."</string>
<string name="fail_reason_could_not_open_file" msgid="7041148341788958325">"לא ניתן היה לפתוח את \"<xliff:g id="FILE_NAME">%s</xliff:g>\"‏: <xliff:g id="EXACT_REASON">%s</xliff:g>."</string>
<string name="fail_reason_could_not_initialize_exporter" msgid="1231982631020480035">"לא ניתן להפעיל את המייצא: \"<xliff:g id="EXACT_REASON">%s</xliff:g>\"."</string>
<string name="fail_reason_no_exportable_contact" msgid="3717046989062541369">"אין אנשי קשר הניתנים לייצוא."</string>
<string name="fail_reason_error_occurred_during_export" msgid="8426833603664168716">"אירעה שגיאה במהלך הייצוא: \"<xliff:g id="EXACT_REASON">%s</xliff:g>\"."</string>
<string name="fail_reason_too_long_filename" msgid="3996610741248972232">"שם הקובץ הדרוש ארוך מדי (\"<xliff:g id="FILENAME">%s</xliff:g>\")"</string>
- <string name="fail_reason_too_many_vcard" product="nosdcard" msgid="8720294715223591581">"באמצעי האחסון קיימים קובצי vCard רבים מדי."</string>
- <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"קיימים בכרטיס ה-SD קובצי vCard רבים מדי."</string>
+ <string name="fail_reason_too_many_vcard" product="nosdcard" msgid="8720294715223591581">"‏באמצעי האחסון קיימים קובצי vCard רבים מדי."</string>
+ <string name="fail_reason_too_many_vcard" product="default" msgid="3793454448838716962">"‏קיימים בכרטיס ה-SD קובצי vCard רבים מדי."</string>
<string name="fail_reason_io_error" msgid="7736686553669161933">"שגיאת קלט/פלט"</string>
<string name="fail_reason_low_memory_during_import" msgid="3277485820827338116">"אין מספיק זיכרון. ייתכן שהקובץ גדול מדי."</string>
- <string name="fail_reason_vcard_parse_error" msgid="514012644716565082">"לא ניתן היה לנתח את ה-vCard מסיבה בלתי צפויה."</string>
+ <string name="fail_reason_vcard_parse_error" msgid="514012644716565082">"‏לא ניתן היה לנתח את ה-vCard מסיבה בלתי צפויה."</string>
<string name="fail_reason_not_supported" msgid="388664373573337601">"הפורמט אינו נתמך."</string>
- <string name="fail_reason_failed_to_collect_vcard_meta_info" msgid="3179066075701123895">"לא ניתן היה לאסוף מטא-מידע של קובצי vCard נתונים."</string>
- <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"לא ניתן היה לייבא קובץ אחד או יותר (%s)."</string>
+ <string name="fail_reason_failed_to_collect_vcard_meta_info" msgid="3179066075701123895">"‏לא ניתן היה לאסוף מטא-מידע של קובצי vCard נתונים."</string>
+ <string name="fail_reason_failed_to_read_files" msgid="9213844535907986665">"‏לא ניתן היה לייבא קובץ אחד או יותר (%s)."</string>
<string name="exporting_vcard_finished_title" msgid="3581883972188707378">"הייצוא של <xliff:g id="FILENAME">%s</xliff:g> הסתיים."</string>
<string name="exporting_vcard_canceled_title" msgid="6993607802553630980">"הייצוא של <xliff:g id="FILENAME">%s</xliff:g> בוטל."</string>
<string name="exporting_contact_list_title" msgid="5663945499580026953">"מייצא נתונים של אנשי קשר"</string>
@@ -185,39 +185,39 @@
<string name="composer_failed_to_get_database_infomation" msgid="7801276776746351694">"לא ניתן היה לקבל מידע ממסד הנתונים."</string>
<string name="composer_has_no_exportable_contact" product="tablet" msgid="1534625366506752907">"אין אנשי קשר הניתנים לייצוא. אם מוגדרים בטאבלט שלך אנשי קשר, ייתכן שספקי נתונים מסוימים אינם מאפשרים ייצוא של אנשי קשר מהטאבלט."</string>
<string name="composer_has_no_exportable_contact" product="default" msgid="7063040740576745307">"אין אנשי קשר הניתנים לייצוא. אם מוגדרים אנשי קשר בטלפון שלך, ייתכן שספקי נתונים מסוימים אינם מאפשרים ייצוא של אנשי קשר מהטלפון."</string>
- <string name="composer_not_initialized" msgid="6514403866246950877">"יישום יצירת ה-vCard לא הופעל כהלכה."</string>
+ <string name="composer_not_initialized" msgid="6514403866246950877">"‏יישום יצירת ה-vCard לא הופעל כהלכה."</string>
<string name="exporting_contact_failed_title" msgid="6059039606302373945">"לא ניתן היה לייצא"</string>
<string name="exporting_contact_failed_message" msgid="3922498776695333328">"נתוני אנשי הקשר לא יוצאו.\nסיבה: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\""</string>
<string name="no_sdcard_message" product="nosdcard" msgid="5131553549320038333">"לא נמצאו אמצעי אחסון."</string>
- <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"לא נמצא כרטיס SD."</string>
+ <string name="no_sdcard_message" product="default" msgid="3246805937562594626">"‏לא נמצא כרטיס SD."</string>
<string name="confirm_export_message" msgid="7234189779260525384">"רשימת אנשי הקשר שלך תיוצא לקובץ: <xliff:g id="VCARD_FILENAME">%s</xliff:g>."</string>
<string name="importing_vcard_description" msgid="7206609625359484356">"מייבא את <xliff:g id="NAME">%s</xliff:g>"</string>
- <string name="reading_vcard_failed_title" msgid="4360989450476024034">"לא ניתן היה לקרוא נתוני vCard"</string>
- <string name="reading_vcard_canceled_title" msgid="1290217818311955708">"קריאת נתוני ה-VCard בוטלה"</string>
- <string name="importing_vcard_finished_title" msgid="5590676758277628951">"הייבוא של קובץ vCard ‏<xliff:g id="FILENAME">%s</xliff:g> הסתיים"</string>
+ <string name="reading_vcard_failed_title" msgid="4360989450476024034">"‏לא ניתן היה לקרוא נתוני vCard"</string>
+ <string name="reading_vcard_canceled_title" msgid="1290217818311955708">"‏קריאת נתוני ה-VCard בוטלה"</string>
+ <string name="importing_vcard_finished_title" msgid="5590676758277628951">"‏הייבוא של קובץ vCard ‏<xliff:g id="FILENAME">%s</xliff:g> הסתיים"</string>
<string name="importing_vcard_canceled_title" msgid="556913863250769870">"הייבוא של <xliff:g id="FILENAME">%s</xliff:g> בוטל"</string>
<string name="vcard_import_will_start_message" msgid="7184603116300604514">"הייבוא של <xliff:g id="FILENAME">%s</xliff:g> יתבצע תוך זמן קצר."</string>
<string name="vcard_import_will_start_message_with_default_name" msgid="2560192057642180334">"ייבוא הקובץ יתבצע תוך זמן קצר."</string>
- <string name="vcard_import_request_rejected_message" msgid="5209363425953891316">"הבקשה לייבוא ה-vCard נדחתה. נסה שוב מאוחר יותר."</string>
+ <string name="vcard_import_request_rejected_message" msgid="5209363425953891316">"‏הבקשה לייבוא ה-vCard נדחתה. נסה שוב מאוחר יותר."</string>
<string name="vcard_export_will_start_message" msgid="2168853666316526278">"הייצוא של <xliff:g id="FILENAME">%s</xliff:g> יתבצע תוך זמן קצר."</string>
- <string name="vcard_export_request_rejected_message" msgid="8044599716727705282">"הבקשה לייצוא ה-vCard נדחתה. נסה שוב מאוחר יותר."</string>
+ <string name="vcard_export_request_rejected_message" msgid="8044599716727705282">"‏הבקשה לייצוא ה-vCard נדחתה. נסה שוב מאוחר יותר."</string>
<string name="vcard_unknown_filename" msgid="4832657686149881554">"איש קשר"</string>
- <string name="caching_vcard_message" msgid="6635485116655518520">"קובצי ה-vCard נשמרים כקבצים באחסון מקומי זמני. הייבוא בפועל יחל בקרוב."</string>
+ <string name="caching_vcard_message" msgid="6635485116655518520">"‏קובצי ה-vCard נשמרים כקבצים באחסון מקומי זמני. הייבוא בפועל יחל בקרוב."</string>
<string name="percentage" msgid="6699653515463625488">"<xliff:g id="PERCENTAGE">%s</xliff:g><xliff:g id="PERCENTSIGN">%%</xliff:g>"</string>
- <string name="vcard_import_failed" msgid="4105296876768072508">"לא ניתן היה לייבא את ה-vCard."</string>
- <string name="import_failure_no_vcard_file" product="nosdcard" msgid="2066107150525521097">"לא נמצאו קובצי vCard באמצעי האחסון."</string>
- <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"לא נמצאו קובצי vCard בכרטיס ה-SD."</string>
- <string name="nfc_vcard_file_name" msgid="305679412445157370">"איש הקשר התקבל באמצעות NFC"</string>
+ <string name="vcard_import_failed" msgid="4105296876768072508">"‏לא ניתן היה לייבא את ה-vCard."</string>
+ <string name="import_failure_no_vcard_file" product="nosdcard" msgid="2066107150525521097">"‏לא נמצאו קובצי vCard באמצעי האחסון."</string>
+ <string name="import_failure_no_vcard_file" product="default" msgid="1748300468382501403">"‏לא נמצאו קובצי vCard בכרטיס ה-SD."</string>
+ <string name="nfc_vcard_file_name" msgid="305679412445157370">"‏איש הקשר התקבל באמצעות NFC"</string>
<string name="confirm_export_title" msgid="3240899428149018226">"לייצא את אנשי הקשר?"</string>
- <string name="select_vcard_title" msgid="1536575036597557639">"בחר קובץ vCard"</string>
- <string name="import_one_vcard_string" msgid="6199149175802496361">"יבא קובץ vCard אחד"</string>
- <string name="import_multiple_vcard_string" msgid="8931879029943141122">"יבא קובצי vCard מרובים"</string>
- <string name="import_all_vcard_string" msgid="1037495558362397535">"יבא את כל קובצי ה-vCard"</string>
- <string name="searching_vcard_message" product="nosdcard" msgid="2703499592557555234">"מחפש נתוני vCard באמצעי האחסון…"</string>
- <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"מחפש נתוני vCard בכרטיס ה-SD…"</string>
+ <string name="select_vcard_title" msgid="1536575036597557639">"‏בחר קובץ vCard"</string>
+ <string name="import_one_vcard_string" msgid="6199149175802496361">"‏יבא קובץ vCard אחד"</string>
+ <string name="import_multiple_vcard_string" msgid="8931879029943141122">"‏יבא קובצי vCard מרובים"</string>
+ <string name="import_all_vcard_string" msgid="1037495558362397535">"‏יבא את כל קובצי ה-vCard"</string>
+ <string name="searching_vcard_message" product="nosdcard" msgid="2703499592557555234">"‏מחפש נתוני vCard באמצעי האחסון…"</string>
+ <string name="searching_vcard_message" product="default" msgid="6108691847266062338">"‏מחפש נתוני vCard בכרטיס ה-SD…"</string>
<string name="caching_vcard_title" msgid="9185299351381102305">"שומר כקובץ שמור"</string>
<string name="scanning_sdcard_failed_message" product="nosdcard" msgid="8442457519490864500">"לא ניתן היה לסרוק את אמצעי האחסון. (סיבה: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
- <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"לא ניתן היה לסרוק את כרטיס ה-SD. (סיבה: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
+ <string name="scanning_sdcard_failed_message" product="default" msgid="1409798274361146091">"‏לא ניתן היה לסרוק את כרטיס ה-SD. (סיבה: \"<xliff:g id="FAIL_REASON">%s</xliff:g>\")"</string>
<string name="progress_notifier_message" msgid="359931715339778107">"מייבא <xliff:g id="CURRENT_NUMBER">%s</xliff:g>/<xliff:g id="TOTAL_NUMBER">%s</xliff:g>‏: <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="export_to_sdcard" product="default" msgid="3665618085563543169">"יצא לאמצעי אחסון"</string>
<string name="share_visible_contacts" msgid="2150662668080757107">"שתף אנשי קשר שמוצגים כעת"</string>
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"יבא/יצא"</string>
<string name="description_send_message" msgid="2630211225573754774">"שלח הודעה אל <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"חייג לטלפון <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"באמצעות <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> באמצעות <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 392d64bb..2ecb2499 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"インポート/エクスポート"</string>
<string name="description_send_message" msgid="2630211225573754774">"<xliff:g id="NAME">%1$s</xliff:g>さんにメッセージを送信"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g>さんの電話"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"更新元: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>、更新元: <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml
index ef71f016..a1954f52 100644
--- a/res/values-ka-rGE/strings.xml
+++ b/res/values-ka-rGE/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"იმპორტი/ექსპორტი"</string>
<string name="description_send_message" msgid="2630211225573754774">"შეტყობინების გაგზავნა <xliff:g id="NAME">%1$s</xliff:g>-ისთვის"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"დარეკვა ტელეფონზე <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"<xliff:g id="SOURCE">%1$s</xliff:g>-ის მეშვეობით"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>, <xliff:g id="SOURCE">%2$s</xliff:g>-ის მეშვეობით"</string>
</resources>
diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml
index 7f1e8166..ee9c14b9 100644
--- a/res/values-km-rKH/strings.xml
+++ b/res/values-km-rKH/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"នាំចេញ/នាំចូល"</string>
<string name="description_send_message" msgid="2630211225573754774">"ផ្ញើ​សារ​ទៅ <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"ហៅ​លេខ <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"តាមរយៈ <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> តាមរយៈ <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index dbcb85ba..717fb128 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"가져오기/내보내기"</string>
<string name="description_send_message" msgid="2630211225573754774">"<xliff:g id="NAME">%1$s</xliff:g>님에게 메시지 보내기"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g>님에게 전화걸기"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"출처: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>(출처: <xliff:g id="SOURCE">%2$s</xliff:g>)"</string>
</resources>
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml
index 12cd411f..f0a517c6 100644
--- a/res/values-lo-rLA/strings.xml
+++ b/res/values-lo-rLA/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"ນຳເຂົ້າ/ສົ່ງອອກ"</string>
<string name="description_send_message" msgid="2630211225573754774">"ສົ່ງ​ຂໍ້​ຄວາມ​ຫາ <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"ໂທຫາເບີ <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"ຜ່ານ <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> ຜ່ານ <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index e6d6d27a..706e3bde 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importuoti / eksportuoti"</string>
<string name="description_send_message" msgid="2630211225573754774">"Siųsti pranešimą <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Skambinti <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"naudojant „<xliff:g id="SOURCE">%1$s</xliff:g>“"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> naudojant „<xliff:g id="SOURCE">%2$s</xliff:g>“"</string>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 619b6813..00432af8 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importēt/eksportēt"</string>
<string name="description_send_message" msgid="2630211225573754774">"Sūtīt īsziņu kontaktpersonai <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Kontaktpersonas <xliff:g id="NAME">%1$s</xliff:g> tālruņa numurs"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"izmantojot <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>, izmantojot <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml
index 54a97301..31ede169 100644
--- a/res/values-mn-rMN/strings.xml
+++ b/res/values-mn-rMN/strings.xml
@@ -241,4 +241,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Импорт/экспорт"</string>
<string name="description_send_message" msgid="2630211225573754774">"<xliff:g id="NAME">%1$s</xliff:g> руу зурвас илгээх"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g> утас руу залгах"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"<xliff:g id="SOURCE">%1$s</xliff:g>-р"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="SOURCE">%2$s</xliff:g>-н <xliff:g id="DATE">%1$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml
index 18d02d5b..43462c97 100644
--- a/res/values-ms-rMY/strings.xml
+++ b/res/values-ms-rMY/strings.xml
@@ -241,4 +241,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Import/eksport"</string>
<string name="description_send_message" msgid="2630211225573754774">"Hantar mesej kepada <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Dail telefon <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"melalui <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> melalui <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index f90cc740..e36184a1 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importering og eksportering"</string>
<string name="description_send_message" msgid="2630211225573754774">"Send melding til <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Ring til <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 2a01e3ae..30d960e4 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importeren/exporteren"</string>
<string name="description_send_message" msgid="2630211225573754774">"Bericht verzenden naar <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Telefoon van <xliff:g id="NAME">%1$s</xliff:g> bellen"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 0cbab555..87a3727f 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importuj/eksportuj"</string>
<string name="description_send_message" msgid="2630211225573754774">"Wyślij wiadomość do: <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Połącz telefonicznie: <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"przez: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> przez: <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a2cc9236..76d47fe0 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importar/exportar"</string>
<string name="description_send_message" msgid="2630211225573754774">"Enviar mensagem para <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Marcar número de telefone de <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"através do <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> através do <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 09464547..6a9cca2f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importar/exportar"</string>
<string name="description_send_message" msgid="2630211225573754774">"Enviar mensagem para <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Discar número de telefone <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index 346d51c5..a70e6ed1 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -455,4 +455,8 @@
<skip />
<!-- no translation found for description_dial_phone_number (7315580540586351853) -->
<skip />
+ <!-- no translation found for contact_status_update_attribution (8419168578670128134) -->
+ <skip />
+ <!-- no translation found for contact_status_update_attribution_with_date (7492465535645607473) -->
+ <skip />
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index a3080bcd..490c8fc5 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importați/exportați"</string>
<string name="description_send_message" msgid="2630211225573754774">"Trimiteți un mesaj la <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Apelați telefon <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"prin <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> prin <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 2e875680..45d596d6 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Импорт/экспорт"</string>
<string name="description_send_message" msgid="2630211225573754774">"Отправить сообщение контакту <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Позвонить контакту <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"в <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> в <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index de34a4cc..eb0ac22b 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Import a export"</string>
<string name="description_send_message" msgid="2630211225573754774">"Poslať správu kontaktu <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Volať na telefón <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"zdroj: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>, zdroj: <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index d534bca6..93b5ceb7 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Uvozi/izvozi"</string>
<string name="description_send_message" msgid="2630211225573754774">"Pošlji sporočilo osebi <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Kliči osebo <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"vir: <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"vir: <xliff:g id="SOURCE">%2$s</xliff:g> – <xliff:g id="DATE">%1$s</xliff:g>"</string>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index ad97e648..cc679596 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Увези/извези"</string>
<string name="description_send_message" msgid="2630211225573754774">"Пошаљи поруку кориснику <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Позови број телефона корисника <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"преко <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> преко <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 0a105334..58a8f31f 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Importera/exportera"</string>
<string name="description_send_message" msgid="2630211225573754774">"Skicka SMS till <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Ring <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"via <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> via <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ca2dcdc4..3de43d36 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -241,4 +241,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Ingiza/hamisha"</string>
<string name="description_send_message" msgid="2630211225573754774">"Tuma ujumbe kwa <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Piga simu <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"kupitia <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> kupitia <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 8eef3fbc..af4ffda7 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"นำเข้า/ส่งออก"</string>
<string name="description_send_message" msgid="2630211225573754774">"ส่งข้อความให้ <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"หมายเลขโทรศัพท์ของ <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"ผ่านทาง <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> ผ่านทาง <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index c873eb2d..cfa1d2e6 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Mag-import/mag-export"</string>
<string name="description_send_message" msgid="2630211225573754774">"Magpadala ng mensahe kay/sa <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"I-dial ang telepono ni/ng <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"sa pamamagitan ng <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> sa pamamagitan ng <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 0f6aadcb..51efc3f0 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"İçe/Dışa Aktar"</string>
<string name="description_send_message" msgid="2630211225573754774">"<xliff:g id="NAME">%1$s</xliff:g> adlı kişiye mesaj gönder"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g> adlı kişinin telefon numarasını çevir"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"<xliff:g id="SOURCE">%1$s</xliff:g> üzerinden"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="SOURCE">%2$s</xliff:g> üzerinden şu zamanda: <xliff:g id="DATE">%1$s</xliff:g>"</string>
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 61a868ea..e7c32916 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Імпорт або експорт"</string>
<string name="description_send_message" msgid="2630211225573754774">"Надіслати повідомлення: <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Набрати номер: <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"через <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> через <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 313656a6..1ac352e3 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Nhập/xuất"</string>
<string name="description_send_message" msgid="2630211225573754774">"Gửi tin nhắn tới <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Quay số điện thoại <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"qua <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> qua <xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 1946b8ef..fb95ab75 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"导入/导出"</string>
<string name="description_send_message" msgid="2630211225573754774">"向<xliff:g id="NAME">%1$s</xliff:g>发短信"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"拨打<xliff:g id="NAME">%1$s</xliff:g>的电话"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"来源:<xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g>,来源:<xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index cbade50f..7ab5de57 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"匯入/匯出"</string>
<string name="description_send_message" msgid="2630211225573754774">"傳送訊息給<xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g>的電話號碼"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"透過 <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> (透過 <xliff:g id="SOURCE">%2$s</xliff:g>)"</string>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 7df41aa4..2902c45f 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"匯入/匯出"</string>
<string name="description_send_message" msgid="2630211225573754774">"傳送訊息給 <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"<xliff:g id="NAME">%1$s</xliff:g> 的電話號碼"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"透過 <xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> (透過 <xliff:g id="SOURCE">%2$s</xliff:g>)"</string>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 19df9fad..017cda49 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -239,4 +239,6 @@
<string name="menu_import_export" msgid="6446229463809981669">"Ngenisa/ thekelisa"</string>
<string name="description_send_message" msgid="2630211225573754774">"Thumela imiyalezo ku-<xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="description_dial_phone_number" msgid="7315580540586351853">"Shayela ifoni <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution" msgid="8419168578670128134">"nge-<xliff:g id="SOURCE">%1$s</xliff:g>"</string>
+ <string name="contact_status_update_attribution_with_date" msgid="7492465535645607473">"<xliff:g id="DATE">%1$s</xliff:g> nge-<xliff:g id="SOURCE">%2$s</xliff:g>"</string>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index f56eee98..113e3952 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -20,7 +20,7 @@
<color name="primary_text_color">#363636</color>
<!-- Divider color for header separator -->
- <color name="secondary_text_color">#777777</color>
+ <color name="secondary_text_color">#888888</color>
<!-- Divider color for header separator -->
<color name="main_header_separator_color">#AAAAAA</color>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3228729c..d4bb1ff9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -720,4 +720,10 @@ a ren't members of any other group. [CHAR LIMIT=25] -->
-->
<string name="description_dial_phone_number">Dial phone <xliff:g id="name">%1$s</xliff:g></string>
+ <!-- Attribution of a contact status update, when the time of update is unknown -->
+ <string name="contact_status_update_attribution">via <xliff:g id="source" example="Google Talk">%1$s</xliff:g></string>
+
+ <!-- Attribution of a contact status update, when the time of update is known -->
+ <string name="contact_status_update_attribution_with_date"><xliff:g id="date" example="3 hours ago">%1$s</xliff:g> via <xliff:g id="source" example="Google Talk">%2$s</xliff:g></string>
+
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index eb6743b7..1bde31c3 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -59,6 +59,16 @@ background and text color. See also android:style/Widget.Holo.TextView.ListSepar
<item name="android:textAllCaps">true</item>
</style>
+ <style name="TextAppearanceMedium" parent="@android:style/TextAppearance.Medium">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#000000</item>
+ </style>
+
+ <style name="TextAppearanceSmall" parent="@android:style/TextAppearance.Small">
+ <item name="android:textSize">12sp</item>
+ <item name="android:textColor">#888888</item>
+ </style>
+
<style name="ListViewStyle" parent="@android:style/Widget.Holo.Light.ListView">
<item name="android:overScrollMode">always</item>
</style>
diff --git a/src/com/android/contacts/common/ContactPhotoManager.java b/src/com/android/contacts/common/ContactPhotoManager.java
index 995201d6..3cde3197 100644
--- a/src/com/android/contacts/common/ContactPhotoManager.java
+++ b/src/com/android/contacts/common/ContactPhotoManager.java
@@ -573,6 +573,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
layers[1] = new BitmapDrawable(mContext.getResources(), cachedBitmap);
TransitionDrawable drawable = new TransitionDrawable(layers);
view.setImageDrawable(drawable);
+ drawable.setCrossFadeEnabled(true);
drawable.startTransition(FADE_TRANSITION_DURATION);
} else {
view.setImageBitmap(cachedBitmap);
diff --git a/src/com/android/contacts/common/ContactsUtils.java b/src/com/android/contacts/common/ContactsUtils.java
new file mode 100644
index 00000000..038ec260
--- /dev/null
+++ b/src/com/android/contacts/common/ContactsUtils.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.test.NeededForTesting;
+import com.android.contacts.common.model.AccountTypeManager;
+
+import java.util.List;
+
+public class ContactsUtils {
+ private static final String TAG = "ContactsUtils";
+
+ private static int sThumbnailSize = -1;
+
+ // TODO find a proper place for the canonical version of these
+ public interface ProviderNames {
+ String YAHOO = "Yahoo";
+ String GTALK = "GTalk";
+ String MSN = "MSN";
+ String ICQ = "ICQ";
+ String AIM = "AIM";
+ String XMPP = "XMPP";
+ String JABBER = "JABBER";
+ String SKYPE = "SKYPE";
+ String QQ = "QQ";
+ }
+
+ /**
+ * This looks up the provider name defined in
+ * ProviderNames from the predefined IM protocol id.
+ * This is used for interacting with the IM application.
+ *
+ * @param protocol the protocol ID
+ * @return the provider name the IM app uses for the given protocol, or null if no
+ * provider is defined for the given protocol
+ * @hide
+ */
+ public static String lookupProviderNameFromId(int protocol) {
+ switch (protocol) {
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return ProviderNames.GTALK;
+ case Im.PROTOCOL_AIM:
+ return ProviderNames.AIM;
+ case Im.PROTOCOL_MSN:
+ return ProviderNames.MSN;
+ case Im.PROTOCOL_YAHOO:
+ return ProviderNames.YAHOO;
+ case Im.PROTOCOL_ICQ:
+ return ProviderNames.ICQ;
+ case Im.PROTOCOL_JABBER:
+ return ProviderNames.JABBER;
+ case Im.PROTOCOL_SKYPE:
+ return ProviderNames.SKYPE;
+ case Im.PROTOCOL_QQ:
+ return ProviderNames.QQ;
+ }
+ return null;
+ }
+
+ /**
+ * Test if the given {@link CharSequence} contains any graphic characters,
+ * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
+ }
+
+ /**
+ * Returns true if two objects are considered equal. Two null references are equal here.
+ */
+ @NeededForTesting
+ public static boolean areObjectsEqual(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /**
+ * Returns true if two {@link Intent}s are both null, or have the same action.
+ */
+ public static final boolean areIntentActionEqual(Intent a, Intent b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ return TextUtils.equals(a.getAction(), b.getAction());
+ }
+
+ public static boolean areContactWritableAccountsAvailable(Context context) {
+ final List<AccountWithDataSet> accounts =
+ AccountTypeManager.getInstance(context).getAccounts(true /* writeable */);
+ return !accounts.isEmpty();
+ }
+
+ public static boolean areGroupWritableAccountsAvailable(Context context) {
+ final List<AccountWithDataSet> accounts =
+ AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+ return !accounts.isEmpty();
+ }
+
+ /**
+ * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+ * can safely be called from the UI thread, as the provider can serve this without performing
+ * a database access
+ */
+ public static int getThumbnailSize(Context context) {
+ if (sThumbnailSize == -1) {
+ final Cursor c = context.getContentResolver().query(
+ DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+ new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
+ try {
+ c.moveToFirst();
+ sThumbnailSize = c.getInt(0);
+ } finally {
+ c.close();
+ }
+ }
+ return sThumbnailSize;
+ }
+
+}
diff --git a/src/com/android/contacts/common/GroupMetaData.java b/src/com/android/contacts/common/GroupMetaData.java
new file mode 100644
index 00000000..fa86ae20
--- /dev/null
+++ b/src/com/android/contacts/common/GroupMetaData.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 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.contacts.common;
+
+/**
+ * Meta-data for a contact group. We load all groups associated with the contact's
+ * constituent accounts.
+ */
+public final class GroupMetaData {
+ private String mAccountName;
+ private String mAccountType;
+ private String mDataSet;
+ private long mGroupId;
+ private String mTitle;
+ private boolean mDefaultGroup;
+ private boolean mFavorites;
+
+ public GroupMetaData(String accountName, String accountType, String dataSet, long groupId,
+ String title, boolean defaultGroup, boolean favorites) {
+ this.mAccountName = accountName;
+ this.mAccountType = accountType;
+ this.mDataSet = dataSet;
+ this.mGroupId = groupId;
+ this.mTitle = title;
+ this.mDefaultGroup = defaultGroup;
+ this.mFavorites = favorites;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public String getAccountType() {
+ return mAccountType;
+ }
+
+ public String getDataSet() {
+ return mDataSet;
+ }
+
+ public long getGroupId() {
+ return mGroupId;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public boolean isDefaultGroup() {
+ return mDefaultGroup;
+ }
+
+ public boolean isFavorites() {
+ return mFavorites;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/contacts/common/list/ContactEntryListAdapter.java b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
index bf9be457..0d13ec25 100644
--- a/src/com/android/contacts/common/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -667,8 +667,13 @@ public abstract class ContactEntryListAdapter extends IndexerListAdapter {
int contactIdColumn, int lookUpKeyColumn) {
long contactId = cursor.getLong(contactIdColumn);
String lookupKey = cursor.getString(lookUpKeyColumn);
- Uri uri = Contacts.getLookupUri(contactId, lookupKey);
long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+ // Remote directories must have a lookup key or we don't have
+ // a working contact URI
+ if (TextUtils.isEmpty(lookupKey) && isRemoteDirectory(directoryId)) {
+ return null;
+ }
+ Uri uri = Contacts.getLookupUri(contactId, lookupKey);
if (directoryId != Directory.DEFAULT) {
uri = uri.buildUpon().appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index 95ac3920..6b9492bf 100644
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -916,7 +916,7 @@ public class ContactListItemView extends ViewGroup
mNameTextView = new TextView(mContext);
mNameTextView.setSingleLine(true);
mNameTextView.setEllipsize(getTextEllipsis());
- mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+ mNameTextView.setTextAppearance(mContext, R.style.TextAppearanceMedium);
// Manually call setActivated() since this view may be added after the first
// setActivated() call toward this whole item view.
mNameTextView.setActivated(isActivated());
@@ -983,9 +983,9 @@ public class ContactListItemView extends ViewGroup
mLabelView = new TextView(mContext);
mLabelView.setSingleLine(true);
mLabelView.setEllipsize(getTextEllipsis());
- mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mLabelView.setTextAppearance(mContext, R.style.TextAppearanceSmall);
if (mPhotoPosition == PhotoPosition.LEFT) {
- mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
+ //mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
mLabelView.setAllCaps(true);
mLabelView.setGravity(Gravity.END);
} else {
@@ -1072,7 +1072,7 @@ public class ContactListItemView extends ViewGroup
mDataView = new TextView(mContext);
mDataView.setSingleLine(true);
mDataView.setEllipsize(getTextEllipsis());
- mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mDataView.setTextAppearance(mContext, R.style.TextAppearanceSmall);
mDataView.setActivated(isActivated());
mDataView.setId(R.id.cliv_data_view);
addView(mDataView);
@@ -1103,7 +1103,6 @@ public class ContactListItemView extends ViewGroup
mSnippetView.setSingleLine(true);
mSnippetView.setEllipsize(getTextEllipsis());
mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
- mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
mSnippetView.setActivated(isActivated());
addView(mSnippetView);
}
@@ -1535,7 +1534,10 @@ public class ContactListItemView extends ViewGroup
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
- if (mBoundsWithoutHeader.contains((int) x, (int) y)) {
+ // If the touch event's coordinates are not within the view's header, then delegate
+ // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
+ // and ignore the touch event.
+ if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointInView(x, y, 0)) {
return super.onTouchEvent(event);
} else {
return true;
diff --git a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
index 9091fc51..47cce8b1 100644
--- a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
+++ b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
@@ -21,6 +21,7 @@ import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -189,7 +190,7 @@ public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactE
pickPhoneNumber(phoneUri);
} else {
final String number = getPhoneNumber(position);
- if (number != null) {
+ if (!TextUtils.isEmpty(number)) {
cacheContactInfo(position);
mListener.onCallNumberDirectly(number);
} else {
diff --git a/src/com/android/contacts/common/list/PinnedHeaderListView.java b/src/com/android/contacts/common/list/PinnedHeaderListView.java
index 034a3dc8..db247b34 100644
--- a/src/com/android/contacts/common/list/PinnedHeaderListView.java
+++ b/src/com/android/contacts/common/list/PinnedHeaderListView.java
@@ -112,11 +112,11 @@ public class PinnedHeaderListView extends AutoScrollListView
private int mHeaderWidth;
public PinnedHeaderListView(Context context) {
- this(context, null, com.android.internal.R.attr.listViewStyle);
+ this(context, null, android.R.attr.listViewStyle);
}
public PinnedHeaderListView(Context context, AttributeSet attrs) {
- this(context, attrs, com.android.internal.R.attr.listViewStyle);
+ this(context, attrs, android.R.attr.listViewStyle);
}
public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
@@ -414,9 +414,14 @@ public class PinnedHeaderListView extends AutoScrollListView
if (mScrollState == SCROLL_STATE_IDLE) {
final int y = (int)ev.getY();
+ final int x = (int)ev.getX();
for (int i = mSize; --i >= 0;) {
PinnedHeader header = mHeaders[i];
- if (header.visible && header.y <= y && header.y + header.height > y) {
+ // For RTL layouts, this also takes into account that the scrollbar is on the left
+ // side.
+ final int padding = getPaddingLeft();
+ if (header.visible && header.y <= y && header.y + header.height > y &&
+ x >= padding && padding + mHeaderWidth >= x) {
mHeaderTouched = true;
if (mScrollToSectionOnHeaderTouch &&
ev.getAction() == MotionEvent.ACTION_DOWN) {
diff --git a/src/com/android/contacts/common/model/Contact.java b/src/com/android/contacts/common/model/Contact.java
new file mode 100644
index 00000000..d5ff0a32
--- /dev/null
+++ b/src/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.util.DataStatus;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user. The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object. Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data. Thus, an instance of this class should be treated as
+ * a read-only object.
+ */
+public class Contact {
+ private enum Status {
+ /** Contact is successfully loaded */
+ LOADED,
+ /** There was an error loading the contact */
+ ERROR,
+ /** Contact is not found */
+ NOT_FOUND,
+ }
+
+ private final Uri mRequestedUri;
+ private final Uri mLookupUri;
+ private final Uri mUri;
+ private final long mDirectoryId;
+ private final String mLookupKey;
+ private final long mId;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+ private final long mPhotoId;
+ private final String mPhotoUri;
+ private final String mDisplayName;
+ private final String mAltDisplayName;
+ private final String mPhoneticName;
+ private final boolean mStarred;
+ private final Integer mPresence;
+ private ImmutableList<RawContact> mRawContacts;
+ private ImmutableMap<Long,DataStatus> mStatuses;
+ private ImmutableList<AccountType> mInvitableAccountTypes;
+
+ private String mDirectoryDisplayName;
+ private String mDirectoryType;
+ private String mDirectoryAccountType;
+ private String mDirectoryAccountName;
+ private int mDirectoryExportSupport;
+
+ private ImmutableList<GroupMetaData> mGroups;
+
+ private byte[] mPhotoBinaryData;
+ private final boolean mSendToVoicemail;
+ private final String mCustomRingtone;
+ private final boolean mIsUserProfile;
+
+ private final Contact.Status mStatus;
+ private final Exception mException;
+
+ /**
+ * Constructor for special results, namely "no contact found" and "error".
+ */
+ private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+ if (status == Status.ERROR && exception == null) {
+ throw new IllegalArgumentException("ERROR result must have exception");
+ }
+ mStatus = status;
+ mException = exception;
+ mRequestedUri = requestedUri;
+ mLookupUri = null;
+ mUri = null;
+ mDirectoryId = -1;
+ mLookupKey = null;
+ mId = -1;
+ mRawContacts = null;
+ mStatuses = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ mPhotoId = -1;
+ mPhotoUri = null;
+ mDisplayName = null;
+ mAltDisplayName = null;
+ mPhoneticName = null;
+ mStarred = false;
+ mPresence = null;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = false;
+ mCustomRingtone = null;
+ mIsUserProfile = false;
+ }
+
+ public static Contact forError(Uri requestedUri, Exception exception) {
+ return new Contact(requestedUri, Status.ERROR, exception);
+ }
+
+ public static Contact forNotFound(Uri requestedUri) {
+ return new Contact(requestedUri, Status.NOT_FOUND, null);
+ }
+
+ /**
+ * Constructor to call when contact was found
+ */
+ public Contact(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
+ long id, long nameRawContactId, int displayNameSource, long photoId,
+ String photoUri, String displayName, String altDisplayName, String phoneticName,
+ boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
+ boolean isUserProfile) {
+ mStatus = Status.LOADED;
+ mException = null;
+ mRequestedUri = requestedUri;
+ mLookupUri = lookupUri;
+ mUri = uri;
+ mDirectoryId = directoryId;
+ mLookupKey = lookupKey;
+ mId = id;
+ mRawContacts = null;
+ mStatuses = null;
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ mPhotoId = photoId;
+ mPhotoUri = photoUri;
+ mDisplayName = displayName;
+ mAltDisplayName = altDisplayName;
+ mPhoneticName = phoneticName;
+ mStarred = starred;
+ mPresence = presence;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = sendToVoicemail;
+ mCustomRingtone = customRingtone;
+ mIsUserProfile = isUserProfile;
+ }
+
+ public Contact(Uri requestedUri, Contact from) {
+ mRequestedUri = requestedUri;
+
+ mStatus = from.mStatus;
+ mException = from.mException;
+ mLookupUri = from.mLookupUri;
+ mUri = from.mUri;
+ mDirectoryId = from.mDirectoryId;
+ mLookupKey = from.mLookupKey;
+ mId = from.mId;
+ mNameRawContactId = from.mNameRawContactId;
+ mDisplayNameSource = from.mDisplayNameSource;
+ mPhotoId = from.mPhotoId;
+ mPhotoUri = from.mPhotoUri;
+ mDisplayName = from.mDisplayName;
+ mAltDisplayName = from.mAltDisplayName;
+ mPhoneticName = from.mPhoneticName;
+ mStarred = from.mStarred;
+ mPresence = from.mPresence;
+ mRawContacts = from.mRawContacts;
+ mStatuses = from.mStatuses;
+ mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+ mDirectoryDisplayName = from.mDirectoryDisplayName;
+ mDirectoryType = from.mDirectoryType;
+ mDirectoryAccountType = from.mDirectoryAccountType;
+ mDirectoryAccountName = from.mDirectoryAccountName;
+ mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+ mGroups = from.mGroups;
+
+ mPhotoBinaryData = from.mPhotoBinaryData;
+ mSendToVoicemail = from.mSendToVoicemail;
+ mCustomRingtone = from.mCustomRingtone;
+ mIsUserProfile = from.mIsUserProfile;
+ }
+
+ /**
+ * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
+ */
+ public void setDirectoryMetaData(String displayName, String directoryType,
+ String accountType, String accountName, int exportSupport) {
+ mDirectoryDisplayName = displayName;
+ mDirectoryType = directoryType;
+ mDirectoryAccountType = accountType;
+ mDirectoryAccountName = accountName;
+ mDirectoryExportSupport = exportSupport;
+ }
+
+ /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+ mPhotoBinaryData = photoBinaryData;
+ }
+
+ /**
+ * Returns the URI for the contact that contains both the lookup key and the ID. This is
+ * the best URI to reference a contact.
+ * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
+ */
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
+ /**
+ * Returns the contact Uri that was passed to the provider to make the query. This is
+ * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
+ * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
+ * always reference the full aggregate contact.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Returns the URI for which this {@link ContactLoader) was initially requested.
+ */
+ public Uri getRequestedUri() {
+ return mRequestedUri;
+ }
+
+ /**
+ * Instantiate a new RawContactDeltaList for this contact.
+ */
+ public RawContactDeltaList createRawContactDeltaList() {
+ return RawContactDeltaList.fromIterator(getRawContacts().iterator());
+ }
+
+ /**
+ * Returns the contact ID.
+ */
+ @VisibleForTesting
+ /* package */ long getId() {
+ return mId;
+ }
+
+ /**
+ * @return true when an exception happened during loading, in which case
+ * {@link #getException} returns the actual exception object.
+ * Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+ * {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+ * and vice versa.
+ */
+ public boolean isError() {
+ return mStatus == Status.ERROR;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ /**
+ * @return true when the specified contact is not found.
+ * Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+ * {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+ * and vice versa.
+ */
+ public boolean isNotFound() {
+ return mStatus == Status.NOT_FOUND;
+ }
+
+ /**
+ * @return true if the specified contact is successfully loaded.
+ * i.e. neither {@link #isError()} nor {@link #isNotFound()}.
+ */
+ public boolean isLoaded() {
+ return mStatus == Status.LOADED;
+ }
+
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+
+ public long getPhotoId() {
+ return mPhotoId;
+ }
+
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public String getAltDisplayName() {
+ return mAltDisplayName;
+ }
+
+ public String getPhoneticName() {
+ return mPhoneticName;
+ }
+
+ public boolean getStarred() {
+ return mStarred;
+ }
+
+ public Integer getPresence() {
+ return mPresence;
+ }
+
+ /**
+ * This can return non-null invitable account types only if the {@link ContactLoader} was
+ * configured to load invitable account types in its constructor.
+ * @return
+ */
+ public ImmutableList<AccountType> getInvitableAccountTypes() {
+ return mInvitableAccountTypes;
+ }
+
+ public ImmutableList<RawContact> getRawContacts() {
+ return mRawContacts;
+ }
+
+ public ImmutableMap<Long, DataStatus> getStatuses() {
+ return mStatuses;
+ }
+
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public boolean isDirectoryEntry() {
+ return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
+ && mDirectoryId != Directory.LOCAL_INVISIBLE;
+ }
+
+ /**
+ * @return true if this is a contact (not group, etc.) with at least one
+ * writable raw-contact, and false otherwise.
+ */
+ public boolean isWritableContact(final Context context) {
+ return getFirstWritableRawContactId(context) != -1;
+ }
+
+ /**
+ * Return the ID of the first raw-contact in the contact data that belongs to a
+ * contact-writable account, or -1 if no such entity exists.
+ */
+ public long getFirstWritableRawContactId(final Context context) {
+ // Directory entries are non-writable
+ if (isDirectoryEntry()) return -1;
+
+ // Iterate through raw-contacts; if we find a writable on, return its ID.
+ for (RawContact rawContact : getRawContacts()) {
+ AccountType accountType = rawContact.getAccountType(context);
+ if (accountType != null && accountType.areContactsWritable()) {
+ return rawContact.getId();
+ }
+ }
+ // No writable raw-contact was found.
+ return -1;
+ }
+
+ public int getDirectoryExportSupport() {
+ return mDirectoryExportSupport;
+ }
+
+ public String getDirectoryDisplayName() {
+ return mDirectoryDisplayName;
+ }
+
+ public String getDirectoryType() {
+ return mDirectoryType;
+ }
+
+ public String getDirectoryAccountType() {
+ return mDirectoryAccountType;
+ }
+
+ public String getDirectoryAccountName() {
+ return mDirectoryAccountName;
+ }
+
+ public byte[] getPhotoBinaryData() {
+ return mPhotoBinaryData;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ if (mRawContacts.size() != 1) {
+ throw new IllegalStateException(
+ "Cannot extract content values from an aggregated contact");
+ }
+
+ RawContact rawContact = mRawContacts.get(0);
+ ArrayList<ContentValues> result = rawContact.getContentValues();
+
+ // If the photo was loaded using the URI, create an entry for the photo
+ // binary data.
+ if (mPhotoId == 0 && mPhotoBinaryData != null) {
+ ContentValues photo = new ContentValues();
+ photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ photo.put(Photo.PHOTO, mPhotoBinaryData);
+ result.add(photo);
+ }
+
+ return result;
+ }
+
+ /**
+ * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+ * load group metadata in its constructor.
+ * @return
+ */
+ public ImmutableList<GroupMetaData> getGroupMetaData() {
+ return mGroups;
+ }
+
+ public boolean isSendToVoicemail() {
+ return mSendToVoicemail;
+ }
+
+ public String getCustomRingtone() {
+ return mCustomRingtone;
+ }
+
+ public boolean isUserProfile() {
+ return mIsUserProfile;
+ }
+
+ @Override
+ public String toString() {
+ return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
+ ",uri=" + mUri + ",status=" + mStatus + "}";
+ }
+
+ /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+ mRawContacts = rawContacts;
+ }
+
+ /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) {
+ mStatuses = statuses;
+ }
+
+ /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+ mInvitableAccountTypes = accountTypes;
+ }
+
+ /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+ mGroups = groups;
+ }
+}
diff --git a/src/com/android/contacts/common/model/ContactLoader.java b/src/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 00000000..ce177b04
--- /dev/null
+++ b/src/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,970 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactLoader extends AsyncTaskLoader<Contact> {
+
+ private static final String TAG = ContactLoader.class.getSimpleName();
+
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /** A short-lived cache that can be set by {@link #cacheResult()} */
+ private static Contact sCachedResult = null;
+
+ private final Uri mRequestedUri;
+ private Uri mLookupUri;
+ private boolean mLoadGroupMetaData;
+ private boolean mLoadInvitableAccountTypes;
+ private boolean mPostViewNotification;
+ private boolean mComputeFormattedPhoneNumber;
+ private Contact mContact;
+ private ForceLoadContentObserver mObserver;
+ private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+
+ public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+ this(context, lookupUri, false, false, postViewNotification, false);
+ }
+
+ public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+ boolean loadInvitableAccountTypes,
+ boolean postViewNotification, boolean computeFormattedPhoneNumber) {
+ super(context);
+ mLookupUri = lookupUri;
+ mRequestedUri = lookupUri;
+ mLoadGroupMetaData = loadGroupMetaData;
+ mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+ mPostViewNotification = postViewNotification;
+ mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+ }
+
+ /**
+ * Projection used for the query that loads all data for the entire contact (except for
+ * social stream items).
+ */
+ private static class ContactQuery {
+ static final String[] COLUMNS = new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ Contacts.Entity.CONTACT_ID,
+ Contacts.Entity.RAW_CONTACT_ID,
+
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
+ RawContacts.DIRTY,
+ RawContacts.VERSION,
+ RawContacts.SOURCE_ID,
+ RawContacts.SYNC1,
+ RawContacts.SYNC2,
+ RawContacts.SYNC3,
+ RawContacts.SYNC4,
+ RawContacts.DELETED,
+ RawContacts.NAME_VERIFIED,
+
+ Contacts.Entity.DATA_ID,
+ Data.DATA1,
+ Data.DATA2,
+ Data.DATA3,
+ Data.DATA4,
+ Data.DATA5,
+ Data.DATA6,
+ Data.DATA7,
+ Data.DATA8,
+ Data.DATA9,
+ Data.DATA10,
+ Data.DATA11,
+ Data.DATA12,
+ Data.DATA13,
+ Data.DATA14,
+ Data.DATA15,
+ Data.SYNC1,
+ Data.SYNC2,
+ Data.SYNC3,
+ Data.SYNC4,
+ Data.DATA_VERSION,
+ Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY,
+ Data.MIMETYPE,
+ Data.RES_PACKAGE,
+
+ GroupMembership.GROUP_SOURCE_ID,
+
+ Data.PRESENCE,
+ Data.CHAT_CAPABILITY,
+ Data.STATUS,
+ Data.STATUS_RES_PACKAGE,
+ Data.STATUS_ICON,
+ Data.STATUS_LABEL,
+ Data.STATUS_TIMESTAMP,
+
+ Contacts.PHOTO_URI,
+ Contacts.SEND_TO_VOICEMAIL,
+ Contacts.CUSTOM_RINGTONE,
+ Contacts.IS_USER_PROFILE,
+ };
+
+ public static final int NAME_RAW_CONTACT_ID = 0;
+ public static final int DISPLAY_NAME_SOURCE = 1;
+ public static final int LOOKUP_KEY = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int ALT_DISPLAY_NAME = 4;
+ public static final int PHONETIC_NAME = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int STARRED = 7;
+ public static final int CONTACT_PRESENCE = 8;
+ public static final int CONTACT_STATUS = 9;
+ public static final int CONTACT_STATUS_TIMESTAMP = 10;
+ public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+ public static final int CONTACT_STATUS_LABEL = 12;
+ public static final int CONTACT_ID = 13;
+ public static final int RAW_CONTACT_ID = 14;
+
+ public static final int ACCOUNT_NAME = 15;
+ public static final int ACCOUNT_TYPE = 16;
+ public static final int DATA_SET = 17;
+ public static final int ACCOUNT_TYPE_AND_DATA_SET = 18;
+ public static final int DIRTY = 19;
+ public static final int VERSION = 20;
+ public static final int SOURCE_ID = 21;
+ public static final int SYNC1 = 22;
+ public static final int SYNC2 = 23;
+ public static final int SYNC3 = 24;
+ public static final int SYNC4 = 25;
+ public static final int DELETED = 26;
+ public static final int NAME_VERIFIED = 27;
+
+ public static final int DATA_ID = 28;
+ public static final int DATA1 = 29;
+ public static final int DATA2 = 30;
+ public static final int DATA3 = 31;
+ public static final int DATA4 = 32;
+ public static final int DATA5 = 33;
+ public static final int DATA6 = 34;
+ public static final int DATA7 = 35;
+ public static final int DATA8 = 36;
+ public static final int DATA9 = 37;
+ public static final int DATA10 = 38;
+ public static final int DATA11 = 39;
+ public static final int DATA12 = 40;
+ public static final int DATA13 = 41;
+ public static final int DATA14 = 42;
+ public static final int DATA15 = 43;
+ public static final int DATA_SYNC1 = 44;
+ public static final int DATA_SYNC2 = 45;
+ public static final int DATA_SYNC3 = 46;
+ public static final int DATA_SYNC4 = 47;
+ public static final int DATA_VERSION = 48;
+ public static final int IS_PRIMARY = 49;
+ public static final int IS_SUPERPRIMARY = 50;
+ public static final int MIMETYPE = 51;
+ public static final int RES_PACKAGE = 52;
+
+ public static final int GROUP_SOURCE_ID = 53;
+
+ public static final int PRESENCE = 54;
+ public static final int CHAT_CAPABILITY = 55;
+ public static final int STATUS = 56;
+ public static final int STATUS_RES_PACKAGE = 57;
+ public static final int STATUS_ICON = 58;
+ public static final int STATUS_LABEL = 59;
+ public static final int STATUS_TIMESTAMP = 60;
+
+ public static final int PHOTO_URI = 61;
+ public static final int SEND_TO_VOICEMAIL = 62;
+ public static final int CUSTOM_RINGTONE = 63;
+ public static final int IS_USER_PROFILE = 64;
+ }
+
+ /**
+ * Projection used for the query that loads all data for the entire contact.
+ */
+ private static class DirectoryQuery {
+ static final String[] COLUMNS = new String[] {
+ Directory.DISPLAY_NAME,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.ACCOUNT_TYPE,
+ Directory.ACCOUNT_NAME,
+ Directory.EXPORT_SUPPORT,
+ };
+
+ public static final int DISPLAY_NAME = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int ACCOUNT_TYPE = 3;
+ public static final int ACCOUNT_NAME = 4;
+ public static final int EXPORT_SUPPORT = 5;
+ }
+
+ private static class GroupQuery {
+ static final String[] COLUMNS = new String[] {
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.DATA_SET,
+ Groups.ACCOUNT_TYPE_AND_DATA_SET,
+ Groups._ID,
+ Groups.TITLE,
+ Groups.AUTO_ADD,
+ Groups.FAVORITES,
+ };
+
+ public static final int ACCOUNT_NAME = 0;
+ public static final int ACCOUNT_TYPE = 1;
+ public static final int DATA_SET = 2;
+ public static final int ACCOUNT_TYPE_AND_DATA_SET = 3;
+ public static final int ID = 4;
+ public static final int TITLE = 5;
+ public static final int AUTO_ADD = 6;
+ public static final int FAVORITES = 7;
+ }
+
+ @Override
+ public Contact loadInBackground() {
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
+ resolver, mLookupUri);
+ final Contact cachedResult = sCachedResult;
+ sCachedResult = null;
+ // Is this the same Uri as what we had before already? In that case, reuse that result
+ final Contact result;
+ final boolean resultIsCached;
+ if (cachedResult != null &&
+ UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
+ // We are using a cached result from earlier. Below, we should make sure
+ // we are not doing any more network or disc accesses
+ result = new Contact(mRequestedUri, cachedResult);
+ resultIsCached = true;
+ } else {
+ if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+ result = loadEncodedContactEntity(uriCurrentFormat);
+ } else {
+ result = loadContactEntity(resolver, uriCurrentFormat);
+ }
+ resultIsCached = false;
+ }
+ if (result.isLoaded()) {
+ if (result.isDirectoryEntry()) {
+ if (!resultIsCached) {
+ loadDirectoryMetaData(result);
+ }
+ } else if (mLoadGroupMetaData) {
+ if (result.getGroupMetaData() == null) {
+ loadGroupMetaData(result);
+ }
+ }
+ if (mComputeFormattedPhoneNumber) {
+ computeFormattedPhoneNumbers(result);
+ }
+ if (!resultIsCached) loadPhotoBinaryData(result);
+
+ // Note ME profile should never have "Add connection"
+ if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+ loadInvitableAccountTypes(result);
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+ return Contact.forError(mRequestedUri, e);
+ }
+ }
+
+ private Contact loadEncodedContactEntity(Uri uri) throws JSONException {
+ final String jsonString = uri.getEncodedFragment();
+ final JSONObject json = new JSONObject(jsonString);
+
+ final long directoryId =
+ Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+ final String displayName = json.getString(Contacts.DISPLAY_NAME);
+ final String altDisplayName = json.optString(
+ Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+ final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+ final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+ final Contact contact = new Contact(
+ uri, uri,
+ mLookupUri,
+ directoryId,
+ null /* lookupKey */,
+ -1 /* id */,
+ -1 /* nameRawContactId */,
+ displayNameSource,
+ 0 /* photoId */,
+ photoUri,
+ displayName,
+ altDisplayName,
+ null /* phoneticName */,
+ false /* starred */,
+ null /* presence */,
+ false /* sendToVoicemail */,
+ null /* customRingtone */,
+ false /* isUserProfile */);
+
+ contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build());
+
+ final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+ final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+ if (accountName != null) {
+ final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+ contact.setDirectoryMetaData(directoryName, null, accountName, accountType,
+ json.optInt(Directory.EXPORT_SUPPORT,
+ Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+ } else {
+ contact.setDirectoryMetaData(directoryName, null, null, null,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+ }
+
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, -1);
+ values.put(Data.CONTACT_ID, -1);
+ final RawContact rawContact = new RawContact(values);
+
+ final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+ final Iterator keys = items.keys();
+ while (keys.hasNext()) {
+ final String mimetype = (String) keys.next();
+
+ // Could be single object or array.
+ final JSONObject obj = items.optJSONObject(mimetype);
+ if (obj == null) {
+ final JSONArray array = items.getJSONArray(mimetype);
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject item = array.getJSONObject(i);
+ processOneRecord(rawContact, item, mimetype);
+ }
+ } else {
+ processOneRecord(rawContact, obj, mimetype);
+ }
+ }
+
+ contact.setRawContacts(new ImmutableList.Builder<RawContact>()
+ .add(rawContact)
+ .build());
+ return contact;
+ }
+
+ private void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+ throws JSONException {
+ final ContentValues itemValues = new ContentValues();
+ itemValues.put(Data.MIMETYPE, mimetype);
+ itemValues.put(Data._ID, -1);
+
+ final Iterator iterator = item.keys();
+ while (iterator.hasNext()) {
+ String name = (String) iterator.next();
+ final Object o = item.get(name);
+ if (o instanceof String) {
+ itemValues.put(name, (String) o);
+ } else if (o instanceof Integer) {
+ itemValues.put(name, (Integer) o);
+ }
+ }
+ rawContact.addDataItemValues(itemValues);
+ }
+
+ private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
+ Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+ Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
+ Contacts.Entity.RAW_CONTACT_ID);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in loadContactEntity");
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ // Create the loaded contact starting with the header data.
+ Contact contact = loadContactHeaderData(cursor, contactUri);
+
+ // Fill in the raw contacts, which is wrapped in an Entity and any
+ // status data. Initially, result has empty entities and statuses.
+ long currentRawContactId = -1;
+ RawContact rawContact = null;
+ ImmutableList.Builder<RawContact> rawContactsBuilder =
+ new ImmutableList.Builder<RawContact>();
+ ImmutableMap.Builder<Long, DataStatus> statusesBuilder =
+ new ImmutableMap.Builder<Long, DataStatus>();
+ do {
+ long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+ if (rawContactId != currentRawContactId) {
+ // First time to see this raw contact id, so create a new entity, and
+ // add it to the result's entities.
+ currentRawContactId = rawContactId;
+ rawContact = new RawContact(loadRawContactValues(cursor));
+ rawContactsBuilder.add(rawContact);
+ }
+ if (!cursor.isNull(ContactQuery.DATA_ID)) {
+ ContentValues data = loadDataValues(cursor);
+ rawContact.addDataItemValues(data);
+
+ if (!cursor.isNull(ContactQuery.PRESENCE)
+ || !cursor.isNull(ContactQuery.STATUS)) {
+ final DataStatus status = new DataStatus(cursor);
+ final long dataId = cursor.getLong(ContactQuery.DATA_ID);
+ statusesBuilder.put(dataId, status);
+ }
+ }
+ } while (cursor.moveToNext());
+
+ contact.setRawContacts(rawContactsBuilder.build());
+ contact.setStatuses(statusesBuilder.build());
+
+ return contact;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
+ * not found, returns null
+ */
+ private void loadPhotoBinaryData(Contact contactData) {
+ // If we have a photo URI, try loading that first.
+ String photoUri = contactData.getPhotoUri();
+ if (photoUri != null) {
+ try {
+ final InputStream inputStream;
+ final AssetFileDescriptor fd;
+ final Uri uri = Uri.parse(photoUri);
+ final String scheme = uri.getScheme();
+ if ("http".equals(scheme) || "https".equals(scheme)) {
+ // Support HTTP urls that might come from extended directories
+ inputStream = new URL(photoUri).openStream();
+ fd = null;
+ } else {
+ fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+ inputStream = fd.createInputStream();
+ }
+ byte[] buffer = new byte[16 * 1024];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = inputStream.read(buffer)) != -1) {
+ baos.write(buffer, 0, size);
+ }
+ contactData.setPhotoBinaryData(baos.toByteArray());
+ } finally {
+ inputStream.close();
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ return;
+ } catch (IOException ioe) {
+ // Just fall back to the case below.
+ }
+ }
+
+ // If we couldn't load from a file, fall back to the data blob.
+ final long photoId = contactData.getPhotoId();
+ if (photoId <= 0) {
+ // No photo ID
+ return;
+ }
+
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ for (DataItem dataItem : rawContact.getDataItems()) {
+ if (dataItem.getId() == photoId) {
+ if (!(dataItem instanceof PhotoDataItem)) {
+ break;
+ }
+
+ final PhotoDataItem photo = (PhotoDataItem) dataItem;
+ contactData.setPhotoBinaryData(photo.getPhoto());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}.
+ */
+ private void loadInvitableAccountTypes(Contact contactData) {
+ final ImmutableList.Builder<AccountType> resultListBuilder =
+ new ImmutableList.Builder<AccountType>();
+ if (!contactData.isUserProfile()) {
+ Map<AccountTypeWithDataSet, AccountType> invitables =
+ AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+ if (!invitables.isEmpty()) {
+ final Map<AccountTypeWithDataSet, AccountType> resultMap =
+ Maps.newHashMap(invitables);
+
+ // Remove the ones that already have a raw contact in the current contact
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+ rawContact.getAccountTypeString(),
+ rawContact.getDataSet());
+ resultMap.remove(type);
+ }
+
+ resultListBuilder.addAll(resultMap.values());
+ }
+ }
+
+ // Set to mInvitableAccountTypes
+ contactData.setInvitableAccountTypes(resultListBuilder.build());
+ }
+
+ /**
+ * Extracts Contact level columns from the cursor.
+ */
+ private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+ final String directoryParameter =
+ contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ final long directoryId = directoryParameter == null
+ ? Directory.DEFAULT
+ : Long.parseLong(directoryParameter);
+ final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+ final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+ final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+ final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+ final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+ final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+ final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+ final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+ final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+ final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+ final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+ Uri lookupUri;
+ if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+ lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+ } else {
+ lookupUri = contactUri;
+ }
+
+ return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
+ contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
+ altDisplayName, phoneticName, starred, presence, sendToVoicemail,
+ customRingtone, isUserProfile);
+ }
+
+ /**
+ * Extracts RawContact level columns from the cursor.
+ */
+ private ContentValues loadRawContactValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
+
+ return cv;
+ }
+
+ /**
+ * Extracts Data level columns from the cursor.
+ */
+ private ContentValues loadDataValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+
+ return cv;
+ }
+
+ private void cursorColumnToContentValues(
+ Cursor cursor, ContentValues values, int index) {
+ switch (cursor.getType(index)) {
+ case Cursor.FIELD_TYPE_NULL:
+ // don't put anything in the content values
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+ break;
+ default:
+ throw new IllegalStateException("Invalid or unhandled data type");
+ }
+ }
+
+ private void loadDirectoryMetaData(Contact result) {
+ long directoryId = result.getDirectoryId();
+
+ Cursor cursor = getContext().getContentResolver().query(
+ ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+ DirectoryQuery.COLUMNS, null, null, null);
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+ final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+ final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+ String directoryType = null;
+ if (!TextUtils.isEmpty(packageName)) {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ Resources resources = pm.getResourcesForApplication(packageName);
+ directoryType = resources.getString(typeResourceId);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Contact directory resource not found: "
+ + packageName + "." + typeResourceId);
+ }
+ }
+
+ result.setDirectoryMetaData(
+ displayName, directoryType, accountType, accountName, exportSupport);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads groups meta-data for all groups associated with all constituent raw contacts'
+ * accounts.
+ */
+ private void loadGroupMetaData(Contact result) {
+ StringBuilder selection = new StringBuilder();
+ ArrayList<String> selectionArgs = new ArrayList<String>();
+ for (RawContact rawContact : result.getRawContacts()) {
+ final String accountName = rawContact.getAccountName();
+ final String accountType = rawContact.getAccountTypeString();
+ final String dataSet = rawContact.getDataSet();
+ if (accountName != null && accountType != null) {
+ if (selection.length() != 0) {
+ selection.append(" OR ");
+ }
+ selection.append(
+ "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+ selectionArgs.add(accountName);
+ selectionArgs.add(accountType);
+
+ if (dataSet != null) {
+ selection.append(" AND " + Groups.DATA_SET + "=?");
+ selectionArgs.add(dataSet);
+ } else {
+ selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+ }
+ selection.append(")");
+ }
+ }
+ final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+ new ImmutableList.Builder<GroupMetaData>();
+ final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
+ GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
+ null);
+ try {
+ while (cursor.moveToNext()) {
+ final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+ final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+ final long groupId = cursor.getLong(GroupQuery.ID);
+ final String title = cursor.getString(GroupQuery.TITLE);
+ final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
+ ? false
+ : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+ final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
+ ? false
+ : cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+ groupListBuilder.add(new GroupMetaData(
+ accountName, accountType, dataSet, groupId, title, defaultGroup,
+ favorites));
+ }
+ } finally {
+ cursor.close();
+ }
+ result.setGroupMetaData(groupListBuilder.build());
+ }
+
+ /**
+ * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+ * number. This function can safely be called several times as no unformatted data is
+ * overwritten
+ */
+ private void computeFormattedPhoneNumbers(Contact contactData) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+ final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+ final int rawContactCount = rawContacts.size();
+ for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+ final RawContact rawContact = rawContacts.get(rawContactIndex);
+ final List<DataItem> dataItems = rawContact.getDataItems();
+ final int dataCount = dataItems.size();
+ for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+ final DataItem dataItem = dataItems.get(dataIndex);
+ if (dataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+ phoneDataItem.computeFormattedPhoneNumber(countryIso);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void deliverResult(Contact result) {
+ unregisterObserver();
+
+ // The creator isn't interested in any further updates
+ if (isReset() || result == null) {
+ return;
+ }
+
+ mContact = result;
+
+ if (result.isLoaded()) {
+ mLookupUri = result.getLookupUri();
+
+ if (!result.isDirectoryEntry()) {
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ getContext().getContentResolver().registerContentObserver(
+ mLookupUri, true, mObserver);
+ }
+
+ if (mPostViewNotification) {
+ // inform the source of the data that this contact is being looked at
+ postViewNotificationToSyncAdapter();
+ }
+ }
+
+ super.deliverResult(mContact);
+ }
+
+ /**
+ * Posts a message to the contributing sync adapters that have opted-in, notifying them
+ * that the contact has just been loaded
+ */
+ private void postViewNotificationToSyncAdapter() {
+ Context context = getContext();
+ for (RawContact rawContact : mContact.getRawContacts()) {
+ final long rawContactId = rawContact.getId();
+ if (mNotifiedRawContactIds.contains(rawContactId)) {
+ continue; // Already notified for this raw contact.
+ }
+ mNotifiedRawContactIds.add(rawContactId);
+ final AccountType accountType = rawContact.getAccountType(context);
+ final String serviceName = accountType.getViewContactNotifyServiceClassName();
+ final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+ if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+ final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Intent intent = new Intent();
+ intent.setClassName(servicePackageName, serviceName);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+ try {
+ context.startService(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending message to source-app", e);
+ }
+ }
+ }
+ }
+
+ private void unregisterObserver() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ /**
+ * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
+ * new result will be delivered
+ */
+ public void upgradeToFullContact() {
+ // Everything requested already? Nothing to do, so let's bail out
+ if (mLoadGroupMetaData && mLoadInvitableAccountTypes
+ && mPostViewNotification && mComputeFormattedPhoneNumber) return;
+
+ mLoadGroupMetaData = true;
+ mLoadInvitableAccountTypes = true;
+ mPostViewNotification = true;
+ mComputeFormattedPhoneNumber = true;
+
+ // Cache the current result, so that we only load the "missing" parts of the contact.
+ cacheResult();
+
+ // Our load parameters have changed, so let's pretend the data has changed. Its the same
+ // thing, essentially.
+ onContentChanged();
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mContact != null) {
+ deliverResult(mContact);
+ }
+
+ if (takeContentChanged() || mContact == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ cancelLoad();
+ unregisterObserver();
+ mContact = null;
+ }
+
+ /**
+ * Caches the result, which is useful when we switch from activity to activity, using the same
+ * contact. If the next load is for a different contact, the cached result will be dropped
+ */
+ public void cacheResult() {
+ if (mContact == null || !mContact.isLoaded()) {
+ sCachedResult = null;
+ } else {
+ sCachedResult = mContact;
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/model/RawContact.java b/src/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 00000000..e5fd06a2
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database.
+ * It has specialized getters/setters for raw contact
+ * items, and also contains a collection of DataItem objects. A RawContact contains the information
+ * from a single account.
+ *
+ * This allows RawContact objects to be thought of as a class with raw contact
+ * fields (like account type, name, data set, sync state, etc.) and a list of
+ * DataItem objects that represent contact information elements (like phone
+ * numbers, email, address, etc.).
+ */
+final public class RawContact implements Parcelable {
+
+ private AccountTypeManager mAccountTypeManager;
+ private final ContentValues mValues;
+ private final ArrayList<NamedDataItem> mDataItems;
+
+ final public static class NamedDataItem implements Parcelable {
+ public final Uri mUri;
+
+ // This use to be a DataItem. DataItem creation is now delayed until the point of request
+ // since there is no benefit to storing them here due to the multiple inheritance.
+ // Eventually instanceof still has to be used anyways to determine which sub-class of
+ // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+ // parcelable.
+ //
+ // Instead of having a common DataItem super class, we should refactor this to be a generic
+ // Object where the object is a concrete class that no longer relies on ContentValues.
+ // (this will also make the classes easier to use).
+ // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+ // worse than having a DataItem.
+ public final ContentValues mContentValues;
+
+ public NamedDataItem(Uri uri, ContentValues values) {
+ this.mUri = uri;
+ this.mContentValues = values;
+ }
+
+ public NamedDataItem(Parcel parcel) {
+ this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+ this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mUri, i);
+ parcel.writeParcelable(mContentValues, i);
+ }
+
+ public static final Parcelable.Creator<NamedDataItem> CREATOR
+ = new Parcelable.Creator<NamedDataItem>() {
+
+ @Override
+ public NamedDataItem createFromParcel(Parcel parcel) {
+ return new NamedDataItem(parcel);
+ }
+
+ @Override
+ public NamedDataItem[] newArray(int i) {
+ return new NamedDataItem[i];
+ }
+ };
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mUri, mContentValues);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ final NamedDataItem other = (NamedDataItem) obj;
+ return Objects.equal(mUri, other.mUri) &&
+ Objects.equal(mContentValues, other.mContentValues);
+ }
+ }
+
+ public static RawContact createFrom(Entity entity) {
+ final ContentValues values = entity.getEntityValues();
+ final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+ RawContact rawContact = new RawContact(values);
+ for (Entity.NamedContentValues subValue : subValues) {
+ rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+ }
+ return rawContact;
+ }
+
+ /**
+ * A RawContact object can be created with or without a context.
+ */
+ public RawContact() {
+ this(new ContentValues());
+ }
+
+ public RawContact(ContentValues values) {
+ mValues = values;
+ mDataItems = new ArrayList<NamedDataItem>();
+ }
+
+ /**
+ * Constructor for the parcelable.
+ *
+ * @param parcel The parcel to de-serialize from.
+ */
+ private RawContact(Parcel parcel) {
+ mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ mDataItems = Lists.newArrayList();
+ parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mValues, i);
+ parcel.writeTypedList(mDataItems);
+ }
+
+ /**
+ * Create for building the parcelable.
+ */
+ public static final Parcelable.Creator<RawContact> CREATOR
+ = new Parcelable.Creator<RawContact>() {
+
+ @Override
+ public RawContact createFromParcel(Parcel parcel) {
+ return new RawContact(parcel);
+ }
+
+ @Override
+ public RawContact[] newArray(int i) {
+ return new RawContact[i];
+ }
+ };
+
+ public AccountTypeManager getAccountTypeManager(Context context) {
+ if (mAccountTypeManager == null) {
+ mAccountTypeManager = AccountTypeManager.getInstance(context);
+ }
+ return mAccountTypeManager;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+
+ /**
+ * Returns the id of the raw contact.
+ */
+ public Long getId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ /**
+ * Returns the account name of the raw contact.
+ */
+ public String getAccountName() {
+ return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+ }
+
+ /**
+ * Returns the account type of the raw contact.
+ */
+ public String getAccountTypeString() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ }
+
+ /**
+ * Returns the data set of the raw contact.
+ */
+ public String getDataSet() {
+ return getValues().getAsString(RawContacts.DATA_SET);
+ }
+
+ /**
+ * Returns the account type and data set of the raw contact.
+ */
+ public String getAccountTypeAndDataSetString() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE_AND_DATA_SET);
+ }
+
+ public boolean isDirty() {
+ return getValues().getAsBoolean(RawContacts.DIRTY);
+ }
+
+ public String getSourceId() {
+ return getValues().getAsString(RawContacts.SOURCE_ID);
+ }
+
+ public String getSync1() {
+ return getValues().getAsString(RawContacts.SYNC1);
+ }
+
+ public String getSync2() {
+ return getValues().getAsString(RawContacts.SYNC2);
+ }
+
+ public String getSync3() {
+ return getValues().getAsString(RawContacts.SYNC3);
+ }
+
+ public String getSync4() {
+ return getValues().getAsString(RawContacts.SYNC4);
+ }
+
+ public boolean isDeleted() {
+ return getValues().getAsBoolean(RawContacts.DELETED);
+ }
+
+ public boolean isNameVerified() {
+ return getValues().getAsBoolean(RawContacts.NAME_VERIFIED);
+ }
+
+ public long getContactId() {
+ return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+ }
+
+ public boolean isStarred() {
+ return getValues().getAsBoolean(Contacts.STARRED);
+ }
+
+ public AccountType getAccountType(Context context) {
+ return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
+ }
+
+ /**
+ * Sets the account name, account type, and data set strings.
+ * Valid combinations for account-name, account-type, data-set
+ * 1) null, null, null (local account)
+ * 2) non-null, non-null, null (valid account without data-set)
+ * 3) non-null, non-null, non-null (valid account with data-set)
+ */
+ private void setAccount(String accountName, String accountType, String dataSet) {
+ final ContentValues values = getValues();
+ if (accountName == null) {
+ if (accountType == null && dataSet == null) {
+ // This is a local account
+ values.putNull(RawContacts.ACCOUNT_NAME);
+ values.putNull(RawContacts.ACCOUNT_TYPE);
+ values.putNull(RawContacts.DATA_SET);
+ return;
+ }
+ } else {
+ if (accountType != null) {
+ // This is a valid account, either with or without a dataSet.
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ if (dataSet == null) {
+ values.putNull(RawContacts.DATA_SET);
+ } else {
+ values.put(RawContacts.DATA_SET, dataSet);
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Not a valid combination of account name, type, and data set.");
+ }
+
+ public void setAccount(AccountWithDataSet accountWithDataSet) {
+ setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
+ }
+
+ public void setAccountToLocal() {
+ setAccount(null, null, null);
+ }
+
+ /**
+ * Creates and inserts a DataItem object that wraps the content values, and returns it.
+ */
+ public void addDataItemValues(ContentValues values) {
+ addNamedDataItemValues(Data.CONTENT_URI, values);
+ }
+
+ public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+ final NamedDataItem namedItem = new NamedDataItem(uri, values);
+ mDataItems.add(namedItem);
+ return namedItem;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ final ArrayList<ContentValues> list = Lists.newArrayListWithCapacity(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(dataItem.mContentValues);
+ }
+ }
+ return list;
+ }
+
+ public List<DataItem> getDataItems() {
+ final ArrayList<DataItem> list = Lists.newArrayListWithCapacity(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(DataItem.createFrom(dataItem.mContentValues));
+ }
+ }
+ return list;
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RawContact: ").append(mValues);
+ for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+ sb.append("\n ").append(namedDataItem.mUri);
+ sb.append("\n -> ").append(namedDataItem.mContentValues);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mValues, mDataItems);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ RawContact other = (RawContact) obj;
+ return Objects.equal(mValues, other.mValues) &&
+ Objects.equal(mDataItems, other.mDataItems);
+ }
+}
diff --git a/src/com/android/contacts/common/model/RawContactDelta.java b/src/com/android/contacts/common/model/RawContactDelta.java
new file mode 100644
index 00000000..7a200418
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDelta.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Profile;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.test.NeededForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Contains a {@link RawContact} and records any modifications separately so the
+ * original {@link RawContact} can be swapped out with a newer version and the
+ * changes still cleanly applied.
+ * <p>
+ * One benefit of this approach is that we can build changes entirely on an
+ * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
+ * <p>
+ * When applying modifications over an {@link RawContact}, we try finding the
+ * original {@link Data#_ID} rows where the modifications took place. If those
+ * rows are missing from the new {@link RawContact}, we know the original data must
+ * be deleted, but to preserve the user modifications we treat as an insert.
+ */
+public class RawContactDelta implements Parcelable {
+ // TODO: optimize by using contentvalues pool, since we allocate so many of them
+
+ private static final String TAG = "EntityDelta";
+ private static final boolean LOGV = false;
+
+ /**
+ * Direct values from {@link Entity#getEntityValues()}.
+ */
+ private ValuesDelta mValues;
+
+ /**
+ * URI used for contacts queries, by default it is set to query raw contacts.
+ * It can be set to query the profile's raw contact(s).
+ */
+ private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
+
+ /**
+ * Internal map of children values from {@link Entity#getSubValues()}, which
+ * we store here sorted into {@link Data#MIMETYPE} bins.
+ */
+ private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
+
+ public RawContactDelta() {
+ }
+
+ public RawContactDelta(ValuesDelta values) {
+ mValues = values;
+ }
+
+ /**
+ * Build an {@link RawContactDelta} using the given {@link RawContact} as a
+ * starting point; the "before" snapshot.
+ */
+ public static RawContactDelta fromBefore(RawContact before) {
+ final RawContactDelta rawContactDelta = new RawContactDelta();
+ rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
+ rawContactDelta.mValues.setIdColumn(RawContacts._ID);
+ for (final ContentValues values : before.getContentValues()) {
+ rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
+ }
+ return rawContactDelta;
+ }
+
+ /**
+ * Merge the "after" values from the given {@link RawContactDelta} onto the
+ * "before" state represented by this {@link RawContactDelta}, discarding any
+ * existing "after" states. This is typically used when re-parenting changes
+ * onto an updated {@link Entity}.
+ */
+ public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
+ // Bail early if trying to merge delete with missing local
+ final ValuesDelta remoteValues = remote.mValues;
+ if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
+
+ // Create local version if none exists yet
+ if (local == null) local = new RawContactDelta();
+
+ if (LOGV) {
+ final Long localVersion = (local.mValues == null) ? null : local.mValues
+ .getAsLong(RawContacts.VERSION);
+ final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
+ Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
+ + localVersion);
+ }
+
+ // Create values if needed, and merge "after" changes
+ local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
+
+ // Find matching local entry for each remote values, or create
+ for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
+ for (ValuesDelta remoteEntry : mimeEntries) {
+ final Long childId = remoteEntry.getId();
+
+ // Find or create local match and merge
+ final ValuesDelta localEntry = local.getEntry(childId);
+ final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
+
+ if (localEntry == null && merged != null) {
+ // No local entry before, so insert
+ local.addEntry(merged);
+ }
+ }
+ }
+
+ return local;
+ }
+
+ public ValuesDelta getValues() {
+ return mValues;
+ }
+
+ public boolean isContactInsert() {
+ return mValues.isInsert();
+ }
+
+ /**
+ * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
+ * which may return null when no entry exists.
+ */
+ public ValuesDelta getPrimaryEntry(String mimeType) {
+ final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+ if (mimeEntries == null) return null;
+
+ for (ValuesDelta entry : mimeEntries) {
+ if (entry.isPrimary()) {
+ return entry;
+ }
+ }
+
+ // When no direct primary, return something
+ return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+ }
+
+ /**
+ * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
+ * @see #getSuperPrimaryEntry(String, boolean)
+ */
+ public ValuesDelta getSuperPrimaryEntry(String mimeType) {
+ return getSuperPrimaryEntry(mimeType, true);
+ }
+
+ /**
+ * Returns the super-primary entry for the given mime type
+ * @param forceSelection if true, will try to return some value even if a super-primary
+ * doesn't exist (may be a primary, or just a random item
+ * @return
+ */
+ @NeededForTesting
+ public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
+ final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+ if (mimeEntries == null) return null;
+
+ ValuesDelta primary = null;
+ for (ValuesDelta entry : mimeEntries) {
+ if (entry.isSuperPrimary()) {
+ return entry;
+ } else if (entry.isPrimary()) {
+ primary = entry;
+ }
+ }
+
+ if (!forceSelection) {
+ return null;
+ }
+
+ // When no direct super primary, return something
+ if (primary != null) {
+ return primary;
+ }
+ return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+ }
+
+ /**
+ * Return the AccountType that this raw-contact belongs to.
+ */
+ public AccountType getRawContactAccountType(Context context) {
+ ContentValues entityValues = getValues().getCompleteValues();
+ String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+ String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+ return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
+ }
+
+ public Long getRawContactId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ public String getAccountName() {
+ return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+ }
+
+ public String getAccountType() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ }
+
+ public String getDataSet() {
+ return getValues().getAsString(RawContacts.DATA_SET);
+ }
+
+ public AccountType getAccountType(AccountTypeManager manager) {
+ return manager.getAccountType(getAccountType(), getDataSet());
+ }
+
+ public boolean isVisible() {
+ return getValues().isVisible();
+ }
+
+ /**
+ * Return the list of child {@link ValuesDelta} from our optimized map,
+ * creating the list if requested.
+ */
+ private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
+ ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
+ if (mimeEntries == null && lazyCreate) {
+ mimeEntries = Lists.newArrayList();
+ mEntries.put(mimeType, mimeEntries);
+ }
+ return mimeEntries;
+ }
+
+ public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
+ return getMimeEntries(mimeType, false);
+ }
+
+ public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
+ final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
+ if (mimeEntries == null) return 0;
+
+ int count = 0;
+ for (ValuesDelta child : mimeEntries) {
+ // Skip deleted items when requesting only visible
+ if (onlyVisible && !child.isVisible()) continue;
+ count++;
+ }
+ return count;
+ }
+
+ public boolean hasMimeEntries(String mimeType) {
+ return mEntries.containsKey(mimeType);
+ }
+
+ public ValuesDelta addEntry(ValuesDelta entry) {
+ final String mimeType = entry.getMimetype();
+ getMimeEntries(mimeType, true).add(entry);
+ return entry;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ ArrayList<ContentValues> values = Lists.newArrayList();
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta entry : mimeEntries) {
+ if (!entry.isDelete()) {
+ values.add(entry.getCompleteValues());
+ }
+ }
+ }
+ return values;
+ }
+
+ /**
+ * Find entry with the given {@link BaseColumns#_ID} value.
+ */
+ public ValuesDelta getEntry(Long childId) {
+ if (childId == null) {
+ // Requesting an "insert" entry, which has no "before"
+ return null;
+ }
+
+ // Search all children for requested entry
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta entry : mimeEntries) {
+ if (childId.equals(entry.getId())) {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the total number of {@link ValuesDelta} contained.
+ */
+ public int getEntryCount(boolean onlyVisible) {
+ int count = 0;
+ for (String mimeType : mEntries.keySet()) {
+ count += getMimeEntriesCount(mimeType, onlyVisible);
+ }
+ return count;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof RawContactDelta) {
+ final RawContactDelta other = (RawContactDelta)object;
+
+ // Equality failed if parent values different
+ if (!other.mValues.equals(mValues)) return false;
+
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ // Equality failed if any children unmatched
+ if (!other.containsEntry(child)) return false;
+ }
+ }
+
+ // Passed all tests, so equal
+ return true;
+ }
+ return false;
+ }
+
+ private boolean containsEntry(ValuesDelta entry) {
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ // Contained if we find any child that matches
+ if (child.equals(entry)) return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Mark this entire object deleted, including any {@link ValuesDelta}.
+ */
+ public void markDeleted() {
+ this.mValues.markDeleted();
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ child.markDeleted();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("\n(");
+ builder.append("Uri=");
+ builder.append(mContactsQueryUri);
+ builder.append(", Values=");
+ builder.append(mValues != null ? mValues.toString() : "null");
+ builder.append(", Entries={");
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ builder.append("\n\t");
+ child.toString(builder);
+ }
+ }
+ builder.append("\n})\n");
+ return builder.toString();
+ }
+
+ /**
+ * Consider building the given {@link ContentProviderOperation.Builder} and
+ * appending it to the given list, which only happens if builder is valid.
+ */
+ private void possibleAdd(ArrayList<ContentProviderOperation> diff,
+ ContentProviderOperation.Builder builder) {
+ if (builder != null) {
+ diff.add(builder.build());
+ }
+ }
+
+ /**
+ * Build a list of {@link ContentProviderOperation} that will assert any
+ * "before" state hasn't changed. This is maintained separately so that all
+ * asserts can take place before any updates occur.
+ */
+ public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
+ final boolean isContactInsert = mValues.isInsert();
+ if (!isContactInsert) {
+ // Assert version is consistent while persisting changes
+ final Long beforeId = mValues.getId();
+ final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+ if (beforeId == null || beforeVersion == null) return;
+
+ final ContentProviderOperation.Builder builder = ContentProviderOperation
+ .newAssertQuery(mContactsQueryUri);
+ builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+ builder.withValue(RawContacts.VERSION, beforeVersion);
+ buildInto.add(builder.build());
+ }
+ }
+
+ /**
+ * Build a list of {@link ContentProviderOperation} that will transform the
+ * current "before" {@link Entity} state into the modified state which this
+ * {@link RawContactDelta} represents.
+ */
+ public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
+ final int firstIndex = buildInto.size();
+
+ final boolean isContactInsert = mValues.isInsert();
+ final boolean isContactDelete = mValues.isDelete();
+ final boolean isContactUpdate = !isContactInsert && !isContactDelete;
+
+ final Long beforeId = mValues.getId();
+
+ Builder builder;
+
+ if (isContactInsert) {
+ // TODO: for now simply disabling aggregation when a new contact is
+ // created on the phone. In the future, will show aggregation suggestions
+ // after saving the contact.
+ mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+ }
+
+ // Build possible operation at Contact level
+ builder = mValues.buildDiff(mContactsQueryUri);
+ possibleAdd(buildInto, builder);
+
+ // Build operations for all children
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ // Ignore children if parent was deleted
+ if (isContactDelete) continue;
+
+ // Use the profile data URI if the contact is the profile.
+ if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
+ builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
+ RawContacts.Data.CONTENT_DIRECTORY));
+ } else {
+ builder = child.buildDiff(Data.CONTENT_URI);
+ }
+
+ if (child.isInsert()) {
+ if (isContactInsert) {
+ // Parent is brand new insert, so back-reference _id
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
+ } else {
+ // Inserting under existing, so fill with known _id
+ builder.withValue(Data.RAW_CONTACT_ID, beforeId);
+ }
+ } else if (isContactInsert && builder != null) {
+ // Child must be insert when Contact insert
+ throw new IllegalArgumentException("When parent insert, child must be also");
+ }
+ possibleAdd(buildInto, builder);
+ }
+ }
+
+ final boolean addedOperations = buildInto.size() > firstIndex;
+ if (addedOperations && isContactUpdate) {
+ // Suspend aggregation while persisting updates
+ builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
+ buildInto.add(firstIndex, builder.build());
+
+ // Restore aggregation mode as last operation
+ builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
+ buildInto.add(builder.build());
+ } else if (isContactInsert) {
+ // Restore aggregation mode as last operation
+ builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+ builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+ builder.withSelection(RawContacts._ID + "=?", new String[1]);
+ builder.withSelectionBackReference(0, firstIndex);
+ buildInto.add(builder.build());
+ }
+ }
+
+ /**
+ * Build a {@link ContentProviderOperation} that changes
+ * {@link RawContacts#AGGREGATION_MODE} to the given value.
+ */
+ protected Builder buildSetAggregationMode(Long beforeId, int mode) {
+ Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+ builder.withValue(RawContacts.AGGREGATION_MODE, mode);
+ builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+ return builder;
+ }
+
+ /** {@inheritDoc} */
+ public int describeContents() {
+ // Nothing special about this parcel
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ public void writeToParcel(Parcel dest, int flags) {
+ final int size = this.getEntryCount(false);
+ dest.writeInt(size);
+ dest.writeParcelable(mValues, flags);
+ dest.writeParcelable(mContactsQueryUri, flags);
+ for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+ for (ValuesDelta child : mimeEntries) {
+ dest.writeParcelable(child, flags);
+ }
+ }
+ }
+
+ public void readFromParcel(Parcel source) {
+ final ClassLoader loader = getClass().getClassLoader();
+ final int size = source.readInt();
+ mValues = source.<ValuesDelta> readParcelable(loader);
+ mContactsQueryUri = source.<Uri> readParcelable(loader);
+ for (int i = 0; i < size; i++) {
+ final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
+ this.addEntry(child);
+ }
+ }
+
+ /**
+ * Used to set the query URI to the profile URI to store profiles.
+ */
+ public void setProfileQueryUri() {
+ mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
+ }
+
+ public static final Parcelable.Creator<RawContactDelta> CREATOR =
+ new Parcelable.Creator<RawContactDelta>() {
+ public RawContactDelta createFromParcel(Parcel in) {
+ final RawContactDelta state = new RawContactDelta();
+ state.readFromParcel(in);
+ return state;
+ }
+
+ public RawContactDelta[] newArray(int size) {
+ return new RawContactDelta[size];
+ }
+ };
+
+}
diff --git a/src/com/android/contacts/common/model/RawContactDeltaList.java b/src/com/android/contacts/common/model/RawContactDeltaList.java
new file mode 100644
index 00000000..f3070c41
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDeltaList.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.model.ValuesDelta;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Container for multiple {@link RawContactDelta} objects, usually when editing
+ * together as an entire aggregate. Provides convenience methods for parceling
+ * and applying another {@link RawContactDeltaList} over it.
+ */
+public class RawContactDeltaList extends ArrayList<RawContactDelta> implements Parcelable {
+ private static final String TAG = RawContactDeltaList.class.getSimpleName();
+ private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
+ private boolean mSplitRawContacts;
+ private long[] mJoinWithRawContactIds;
+
+ public RawContactDeltaList() {
+ }
+
+ /**
+ * Create an {@link RawContactDeltaList} based on {@link Contacts} specified by the
+ * given query parameters. This closes the {@link EntityIterator} when
+ * finished, so it doesn't subscribe to updates.
+ */
+ public static RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
+ String selection, String[] selectionArgs, String sortOrder) {
+ final EntityIterator iterator = RawContacts.newEntityIterator(
+ resolver.query(entityUri, null, selection, selectionArgs, sortOrder));
+ try {
+ return fromIterator(iterator);
+ } finally {
+ iterator.close();
+ }
+ }
+
+ /**
+ * Create an {@link RawContactDeltaList} that contains the entities of the Iterator as before
+ * values. This function can be passed an iterator of Entity objects or an iterator of
+ * RawContact objects.
+ */
+ public static RawContactDeltaList fromIterator(Iterator<?> iterator) {
+ final RawContactDeltaList state = new RawContactDeltaList();
+ state.addAll(iterator);
+ return state;
+ }
+
+ public void addAll(Iterator<?> iterator) {
+ // Perform background query to pull contact details
+ while (iterator.hasNext()) {
+ // Read all contacts into local deltas to prepare for edits
+ Object nextObject = iterator.next();
+ final RawContact before = nextObject instanceof Entity
+ ? RawContact.createFrom((Entity) nextObject)
+ : (RawContact) nextObject;
+ final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before);
+ add(rawContactDelta);
+ }
+ }
+
+ /**
+ * Merge the "after" values from the given {@link RawContactDeltaList}, discarding any
+ * previous "after" states. This is typically used when re-parenting user
+ * edits onto an updated {@link RawContactDeltaList}.
+ */
+ public static RawContactDeltaList mergeAfter(RawContactDeltaList local,
+ RawContactDeltaList remote) {
+ if (local == null) local = new RawContactDeltaList();
+
+ // For each entity in the remote set, try matching over existing
+ for (RawContactDelta remoteEntity : remote) {
+ final Long rawContactId = remoteEntity.getValues().getId();
+
+ // Find or create local match and merge
+ final RawContactDelta localEntity = local.getByRawContactId(rawContactId);
+ final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity);
+
+ if (localEntity == null && merged != null) {
+ // No local entry before, so insert
+ local.add(merged);
+ }
+ }
+
+ return local;
+ }
+
+ /**
+ * Build a list of {@link ContentProviderOperation} that will transform all
+ * the "before" {@link Entity} states into the modified state which all
+ * {@link RawContactDelta} objects represent. This method specifically creates
+ * any {@link AggregationExceptions} rules needed to groups edits together.
+ */
+ public ArrayList<ContentProviderOperation> buildDiff() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "buildDiff: list=" + toString());
+ }
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+
+ final long rawContactId = this.findRawContactId();
+ int firstInsertRow = -1;
+
+ // First pass enforces versions remain consistent
+ for (RawContactDelta delta : this) {
+ delta.buildAssert(diff);
+ }
+
+ final int assertMark = diff.size();
+ int backRefs[] = new int[size()];
+
+ int rawContactIndex = 0;
+
+ // Second pass builds actual operations
+ for (RawContactDelta delta : this) {
+ final int firstBatch = diff.size();
+ final boolean isInsert = delta.isContactInsert();
+ backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
+
+ delta.buildDiff(diff);
+
+ // If the user chose to join with some other existing raw contact(s) at save time,
+ // add aggregation exceptions for all those raw contacts.
+ if (mJoinWithRawContactIds != null) {
+ for (Long joinedRawContactId : mJoinWithRawContactIds) {
+ final Builder builder = beginKeepTogether();
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
+ if (rawContactId != -1) {
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
+ } else {
+ builder.withValueBackReference(
+ AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+ }
+ diff.add(builder.build());
+ }
+ }
+
+ // Only create rules for inserts
+ if (!isInsert) continue;
+
+ // If we are going to split all contacts, there is no point in first combining them
+ if (mSplitRawContacts) continue;
+
+ if (rawContactId != -1) {
+ // Has existing contact, so bind to it strongly
+ final Builder builder = beginKeepTogether();
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+ diff.add(builder.build());
+
+ } else if (firstInsertRow == -1) {
+ // First insert case, so record row
+ firstInsertRow = firstBatch;
+
+ } else {
+ // Additional insert case, so point at first insert
+ final Builder builder = beginKeepTogether();
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
+ firstInsertRow);
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+ diff.add(builder.build());
+ }
+ }
+
+ if (mSplitRawContacts) {
+ buildSplitContactDiff(diff, backRefs);
+ }
+
+ // No real changes if only left with asserts
+ if (diff.size() == assertMark) {
+ diff.clear();
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "buildDiff: ops=" + diffToString(diff));
+ }
+ return diff;
+ }
+
+ private static String diffToString(ArrayList<ContentProviderOperation> ops) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[\n");
+ for (ContentProviderOperation op : ops) {
+ sb.append(op.toString());
+ sb.append(",\n");
+ }
+ sb.append("]\n");
+ return sb.toString();
+ }
+
+ /**
+ * Start building a {@link ContentProviderOperation} that will keep two
+ * {@link RawContacts} together.
+ */
+ protected Builder beginKeepTogether() {
+ final Builder builder = ContentProviderOperation
+ .newUpdate(AggregationExceptions.CONTENT_URI);
+ builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+ return builder;
+ }
+
+ /**
+ * Builds {@link AggregationExceptions} to split all constituent raw contacts into
+ * separate contacts.
+ */
+ private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
+ int[] backRefs) {
+ int count = size();
+ for (int i = 0; i < count; i++) {
+ for (int j = 0; j < count; j++) {
+ if (i != j) {
+ buildSplitContactDiff(diff, i, j, backRefs);
+ }
+ }
+ }
+ }
+
+ /**
+ * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
+ */
+ private void buildSplitContactDiff(ArrayList<ContentProviderOperation> diff, int index1,
+ int index2, int[] backRefs) {
+ Builder builder =
+ ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+ builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
+
+ Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
+ int backRef1 = backRefs[index1];
+ if (rawContactId1 != null && rawContactId1 >= 0) {
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+ } else if (backRef1 >= 0) {
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1);
+ } else {
+ return;
+ }
+
+ Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
+ int backRef2 = backRefs[index2];
+ if (rawContactId2 != null && rawContactId2 >= 0) {
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+ } else if (backRef2 >= 0) {
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2);
+ } else {
+ return;
+ }
+
+ diff.add(builder.build());
+ }
+
+ /**
+ * Search all contained {@link RawContactDelta} for the first one with an
+ * existing {@link RawContacts#_ID} value. Usually used when creating
+ * {@link AggregationExceptions} during an update.
+ */
+ public long findRawContactId() {
+ for (RawContactDelta delta : this) {
+ final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
+ if (rawContactId != null && rawContactId >= 0) {
+ return rawContactId;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}.
+ */
+ public Long getRawContactId(int index) {
+ if (index >= 0 && index < this.size()) {
+ final RawContactDelta delta = this.get(index);
+ final ValuesDelta values = delta.getValues();
+ if (values.isVisible()) {
+ return values.getAsLong(RawContacts._ID);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find the raw-contact (an {@link RawContactDelta}) with the specified ID.
+ */
+ public RawContactDelta getByRawContactId(Long rawContactId) {
+ final int index = this.indexOfRawContactId(rawContactId);
+ return (index == -1) ? null : this.get(index);
+ }
+
+ /**
+ * Find index of given {@link RawContacts#_ID} when present.
+ */
+ public int indexOfRawContactId(Long rawContactId) {
+ if (rawContactId == null) return -1;
+ final int size = this.size();
+ for (int i = 0; i < size; i++) {
+ final Long currentId = getRawContactId(i);
+ if (rawContactId.equals(currentId)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1.
+ * */
+ public int indexOfFirstWritableRawContact(Context context) {
+ // Find the first writable entity.
+ int entityIndex = 0;
+ for (RawContactDelta delta : this) {
+ if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
+ entityIndex++;
+ }
+ return -1;
+ }
+
+ /** Return the first RawContactDelta corresponding to a writable raw-contact, or null. */
+ public RawContactDelta getFirstWritableRawContact(Context context) {
+ final int index = indexOfFirstWritableRawContact(context);
+ return (index == -1) ? null : get(index);
+ }
+
+ public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
+ ValuesDelta primary = null;
+ ValuesDelta randomEntry = null;
+ for (RawContactDelta delta : this) {
+ final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
+ if (mimeEntries == null) return null;
+
+ for (ValuesDelta entry : mimeEntries) {
+ if (entry.isSuperPrimary()) {
+ return entry;
+ } else if (primary == null && entry.isPrimary()) {
+ primary = entry;
+ } else if (randomEntry == null) {
+ randomEntry = entry;
+ }
+ }
+ }
+ // When no direct super primary, return something
+ if (primary != null) {
+ return primary;
+ }
+ return randomEntry;
+ }
+
+ /**
+ * Sets a flag that will split ("explode") the raw_contacts into seperate contacts
+ */
+ public void markRawContactsForSplitting() {
+ mSplitRawContacts = true;
+ }
+
+ public boolean isMarkedForSplitting() {
+ return mSplitRawContacts;
+ }
+
+ public void setJoinWithRawContacts(long[] rawContactIds) {
+ mJoinWithRawContactIds = rawContactIds;
+ }
+
+ public boolean isMarkedForJoining() {
+ return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int describeContents() {
+ // Nothing special about this parcel
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ final int size = this.size();
+ dest.writeInt(size);
+ for (RawContactDelta delta : this) {
+ dest.writeParcelable(delta, flags);
+ }
+ dest.writeLongArray(mJoinWithRawContactIds);
+ dest.writeInt(mSplitRawContacts ? 1 : 0);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void readFromParcel(Parcel source) {
+ final ClassLoader loader = getClass().getClassLoader();
+ final int size = source.readInt();
+ for (int i = 0; i < size; i++) {
+ this.add(source.<RawContactDelta> readParcelable(loader));
+ }
+ mJoinWithRawContactIds = source.createLongArray();
+ mSplitRawContacts = source.readInt() != 0;
+ }
+
+ public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
+ new Parcelable.Creator<RawContactDeltaList>() {
+ @Override
+ public RawContactDeltaList createFromParcel(Parcel in) {
+ final RawContactDeltaList state = new RawContactDeltaList();
+ state.readFromParcel(in);
+ return state;
+ }
+
+ @Override
+ public RawContactDeltaList[] newArray(int size) {
+ return new RawContactDeltaList[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ sb.append("Split=");
+ sb.append(mSplitRawContacts);
+ sb.append(", Join=[");
+ sb.append(Arrays.toString(mJoinWithRawContactIds));
+ sb.append("], Values=");
+ sb.append(super.toString());
+ sb.append(")");
+ return sb.toString();
+ }
+}
diff --git a/src/com/android/contacts/common/model/RawContactModifier.java b/src/com/android/contacts/common/model/RawContactModifier.java
new file mode 100644
index 00000000..0cd243c5
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactModifier.java
@@ -0,0 +1,1427 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.DateUtils;
+import com.android.contacts.common.util.NameConverter;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.EventEditType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Helper methods for modifying an {@link RawContactDelta}, such as inserting
+ * new rows, or enforcing {@link AccountType}.
+ */
+public class RawContactModifier {
+ private static final String TAG = RawContactModifier.class.getSimpleName();
+
+ /** Set to true in order to view logs on entity operations */
+ private static final boolean DEBUG = false;
+
+ /**
+ * For the given {@link RawContactDelta}, determine if the given
+ * {@link DataKind} could be inserted under specific
+ * {@link AccountType}.
+ */
+ public static boolean canInsert(RawContactDelta state, DataKind kind) {
+ // Insert possible when have valid types and under overall maximum
+ final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true);
+ final boolean validTypes = hasValidTypes(state, kind);
+ final boolean validOverall = (kind.typeOverallMax == -1)
+ || (visibleCount < kind.typeOverallMax);
+ return (validTypes && validOverall);
+ }
+
+ public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
+ if (RawContactModifier.hasEditTypes(kind)) {
+ return (getValidTypes(state, kind).size() > 0);
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Ensure that at least one of the given {@link DataKind} exists in the
+ * given {@link RawContactDelta} state, and try creating one if none exist.
+ * @return The child (either newly created or the first existing one), or null if the
+ * account doesn't support this {@link DataKind}.
+ */
+ public static ValuesDelta ensureKindExists(
+ RawContactDelta state, AccountType accountType, String mimeType) {
+ final DataKind kind = accountType.getKindForMimetype(mimeType);
+ final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
+
+ if (kind != null) {
+ if (hasChild) {
+ // Return the first entry.
+ return state.getMimeEntries(mimeType).get(0);
+ } else {
+ // Create child when none exists and valid kind
+ final ValuesDelta child = insertChild(state, kind);
+ if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+ child.setFromTemplate(true);
+ }
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * For the given {@link RawContactDelta} and {@link DataKind}, return the
+ * list possible {@link EditType} options available based on
+ * {@link AccountType}.
+ */
+ public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind) {
+ return getValidTypes(state, kind, null, true, null);
+ }
+
+ /**
+ * For the given {@link RawContactDelta} and {@link DataKind}, return the
+ * list possible {@link EditType} options available based on
+ * {@link AccountType}.
+ *
+ * @param forceInclude Always include this {@link EditType} in the returned
+ * list, even when an otherwise-invalid choice. This is useful
+ * when showing a dialog that includes the current type.
+ */
+ public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
+ EditType forceInclude) {
+ return getValidTypes(state, kind, forceInclude, true, null);
+ }
+
+ /**
+ * For the given {@link RawContactDelta} and {@link DataKind}, return the
+ * list possible {@link EditType} options available based on
+ * {@link AccountType}.
+ *
+ * @param forceInclude Always include this {@link EditType} in the returned
+ * list, even when an otherwise-invalid choice. This is useful
+ * when showing a dialog that includes the current type.
+ * @param includeSecondary If true, include any valid types marked as
+ * {@link EditType#secondary}.
+ * @param typeCount When provided, will be used for the frequency count of
+ * each {@link EditType}, otherwise built using
+ * {@link #getTypeFrequencies(RawContactDelta, DataKind)}.
+ */
+ private static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
+ EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
+ final ArrayList<EditType> validTypes = new ArrayList<EditType>();
+
+ // Bail early if no types provided
+ if (!hasEditTypes(kind)) return validTypes;
+
+ if (typeCount == null) {
+ // Build frequency counts if not provided
+ typeCount = getTypeFrequencies(state, kind);
+ }
+
+ // Build list of valid types
+ final int overallCount = typeCount.get(FREQUENCY_TOTAL);
+ for (EditType type : kind.typeList) {
+ final boolean validOverall = (kind.typeOverallMax == -1 ? true
+ : overallCount < kind.typeOverallMax);
+ final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
+ .get(type.rawValue) < type.specificMax);
+ final boolean validSecondary = (includeSecondary ? true : !type.secondary);
+ final boolean forcedInclude = type.equals(forceInclude);
+ if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
+ // Type is valid when no limit, under limit, or forced include
+ validTypes.add(type);
+ }
+ }
+
+ return validTypes;
+ }
+
+ private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
+
+ /**
+ * Count up the frequency that each {@link EditType} appears in the given
+ * {@link RawContactDelta}. The returned {@link SparseIntArray} maps from
+ * {@link EditType#rawValue} to counts, with the total overall count stored
+ * as {@link #FREQUENCY_TOTAL}.
+ */
+ private static SparseIntArray getTypeFrequencies(RawContactDelta state, DataKind kind) {
+ final SparseIntArray typeCount = new SparseIntArray();
+
+ // Find all entries for this kind, bailing early if none found
+ final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
+ if (mimeEntries == null) return typeCount;
+
+ int totalCount = 0;
+ for (ValuesDelta entry : mimeEntries) {
+ // Only count visible entries
+ if (!entry.isVisible()) continue;
+ totalCount++;
+
+ final EditType type = getCurrentType(entry, kind);
+ if (type != null) {
+ final int count = typeCount.get(type.rawValue);
+ typeCount.put(type.rawValue, count + 1);
+ }
+ }
+ typeCount.put(FREQUENCY_TOTAL, totalCount);
+ return typeCount;
+ }
+
+ /**
+ * Check if the given {@link DataKind} has multiple types that should be
+ * displayed for users to pick.
+ */
+ public static boolean hasEditTypes(DataKind kind) {
+ return kind.typeList != null && kind.typeList.size() > 0;
+ }
+
+ /**
+ * Find the {@link EditType} that describes the given
+ * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
+ * the possible types.
+ */
+ public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
+ final Long rawValue = entry.getAsLong(kind.typeColumn);
+ if (rawValue == null) return null;
+ return getType(kind, rawValue.intValue());
+ }
+
+ /**
+ * Find the {@link EditType} that describes the given {@link ContentValues} row,
+ * assuming the given {@link DataKind} dictates the possible types.
+ */
+ public static EditType getCurrentType(ContentValues entry, DataKind kind) {
+ if (kind.typeColumn == null) return null;
+ final Integer rawValue = entry.getAsInteger(kind.typeColumn);
+ if (rawValue == null) return null;
+ return getType(kind, rawValue);
+ }
+
+ /**
+ * Find the {@link EditType} that describes the given {@link Cursor} row,
+ * assuming the given {@link DataKind} dictates the possible types.
+ */
+ public static EditType getCurrentType(Cursor cursor, DataKind kind) {
+ if (kind.typeColumn == null) return null;
+ final int index = cursor.getColumnIndex(kind.typeColumn);
+ if (index == -1) return null;
+ final int rawValue = cursor.getInt(index);
+ return getType(kind, rawValue);
+ }
+
+ /**
+ * Find the {@link EditType} with the given {@link EditType#rawValue}.
+ */
+ public static EditType getType(DataKind kind, int rawValue) {
+ for (EditType type : kind.typeList) {
+ if (type.rawValue == rawValue) {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the precedence for the the given {@link EditType#rawValue}, where
+ * lower numbers are higher precedence.
+ */
+ public static int getTypePrecedence(DataKind kind, int rawValue) {
+ for (int i = 0; i < kind.typeList.size(); i++) {
+ final EditType type = kind.typeList.get(i);
+ if (type.rawValue == rawValue) {
+ return i;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+
+ /**
+ * Find the best {@link EditType} for a potential insert. The "best" is the
+ * first primary type that doesn't already exist. When all valid types
+ * exist, we pick the last valid option.
+ */
+ public static EditType getBestValidType(RawContactDelta state, DataKind kind,
+ boolean includeSecondary, int exactValue) {
+ // Shortcut when no types
+ if (kind.typeColumn == null) return null;
+
+ // Find type counts and valid primary types, bail if none
+ final SparseIntArray typeCount = getTypeFrequencies(state, kind);
+ final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
+ typeCount);
+ if (validTypes.size() == 0) return null;
+
+ // Keep track of the last valid type
+ final EditType lastType = validTypes.get(validTypes.size() - 1);
+
+ // Remove any types that already exist
+ Iterator<EditType> iterator = validTypes.iterator();
+ while (iterator.hasNext()) {
+ final EditType type = iterator.next();
+ final int count = typeCount.get(type.rawValue);
+
+ if (exactValue == type.rawValue) {
+ // Found exact value match
+ return type;
+ }
+
+ if (count > 0) {
+ // Type already appears, so don't consider
+ iterator.remove();
+ }
+ }
+
+ // Use the best remaining, otherwise the last valid
+ if (validTypes.size() > 0) {
+ return validTypes.get(0);
+ } else {
+ return lastType;
+ }
+ }
+
+ /**
+ * Insert a new child of kind {@link DataKind} into the given
+ * {@link RawContactDelta}. Tries using the best {@link EditType} found using
+ * {@link #getBestValidType(RawContactDelta, DataKind, boolean, int)}.
+ */
+ public static ValuesDelta insertChild(RawContactDelta state, DataKind kind) {
+ // First try finding a valid primary
+ EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
+ if (bestType == null) {
+ // No valid primary found, so expand search to secondary
+ bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
+ }
+ return insertChild(state, kind, bestType);
+ }
+
+ /**
+ * Insert a new child of kind {@link DataKind} into the given
+ * {@link RawContactDelta}, marked with the given {@link EditType}.
+ */
+ public static ValuesDelta insertChild(RawContactDelta state, DataKind kind, EditType type) {
+ // Bail early if invalid kind
+ if (kind == null) return null;
+ final ContentValues after = new ContentValues();
+
+ // Our parent CONTACT_ID is provided later
+ after.put(Data.MIMETYPE, kind.mimeType);
+
+ // Fill-in with any requested default values
+ if (kind.defaultValues != null) {
+ after.putAll(kind.defaultValues);
+ }
+
+ if (kind.typeColumn != null && type != null) {
+ // Set type, if provided
+ after.put(kind.typeColumn, type.rawValue);
+ }
+
+ final ValuesDelta child = ValuesDelta.fromAfter(after);
+ state.addEntry(child);
+ return child;
+ }
+
+ /**
+ * Processing to trim any empty {@link ValuesDelta} and {@link RawContactDelta}
+ * from the given {@link RawContactDeltaList}, assuming the given {@link AccountTypeManager}
+ * dictates the structure for various fields. This method ignores rows not
+ * described by the {@link AccountType}.
+ */
+ public static void trimEmpty(RawContactDeltaList set, AccountTypeManager accountTypes) {
+ for (RawContactDelta state : set) {
+ ValuesDelta values = state.getValues();
+ final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+ final String dataSet = values.getAsString(RawContacts.DATA_SET);
+ final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+ trimEmpty(state, type);
+ }
+ }
+
+ public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
+ if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
+ return true;
+ }
+
+ for (RawContactDelta state : set) {
+ ValuesDelta values = state.getValues();
+ final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+ final String dataSet = values.getAsString(RawContacts.DATA_SET);
+ final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+ if (hasChanges(state, type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Processing to trim any empty {@link ValuesDelta} rows from the given
+ * {@link RawContactDelta}, assuming the given {@link AccountType} dictates
+ * the structure for various fields. This method ignores rows not described
+ * by the {@link AccountType}.
+ */
+ public static void trimEmpty(RawContactDelta state, AccountType accountType) {
+ boolean hasValues = false;
+
+ // Walk through entries for each well-known kind
+ for (DataKind kind : accountType.getSortedDataKinds()) {
+ final String mimeType = kind.mimeType;
+ final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+ if (entries == null) continue;
+
+ for (ValuesDelta entry : entries) {
+ // Skip any values that haven't been touched
+ final boolean touched = entry.isInsert() || entry.isUpdate();
+ if (!touched) {
+ hasValues = true;
+ continue;
+ }
+
+ // Test and remove this row if empty and it isn't a photo from google
+ final boolean isGoogleAccount = TextUtils.equals(GoogleAccountType.ACCOUNT_TYPE,
+ state.getValues().getAsString(RawContacts.ACCOUNT_TYPE));
+ final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
+ final boolean isGooglePhoto = isPhoto && isGoogleAccount;
+
+ if (RawContactModifier.isEmpty(entry, kind) && !isGooglePhoto) {
+ if (DEBUG) {
+ Log.v(TAG, "Trimming: " + entry.toString());
+ }
+ entry.markDeleted();
+ } else if (!entry.isFromTemplate()) {
+ hasValues = true;
+ }
+ }
+ }
+ if (!hasValues) {
+ // Trim overall entity if no children exist
+ state.markDeleted();
+ }
+ }
+
+ private static boolean hasChanges(RawContactDelta state, AccountType accountType) {
+ for (DataKind kind : accountType.getSortedDataKinds()) {
+ final String mimeType = kind.mimeType;
+ final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+ if (entries == null) continue;
+
+ for (ValuesDelta entry : entries) {
+ // An empty Insert must be ignored, because it won't save anything (an example
+ // is an empty name that stays empty)
+ final boolean isRealInsert = entry.isInsert() && !isEmpty(entry, kind);
+ if (isRealInsert || entry.isUpdate() || entry.isDelete()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Test if the given {@link ValuesDelta} would be considered "empty" in
+ * terms of {@link DataKind#fieldList}.
+ */
+ public static boolean isEmpty(ValuesDelta values, DataKind kind) {
+ if (Photo.CONTENT_ITEM_TYPE.equals(kind.mimeType)) {
+ return values.isInsert() && values.getAsByteArray(Photo.PHOTO) == null;
+ }
+
+ // No defined fields mean this row is always empty
+ if (kind.fieldList == null) return true;
+
+ for (EditField field : kind.fieldList) {
+ // If any field has values, we're not empty
+ final String value = values.getAsString(field.column);
+ if (ContactsUtils.isGraphic(value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Compares corresponding fields in values1 and values2. Only the fields
+ * declared by the DataKind are taken into consideration.
+ */
+ protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) {
+ if (kind.fieldList == null) return false;
+
+ for (EditField field : kind.fieldList) {
+ final String value1 = values1.getAsString(field.column);
+ final String value2 = values2.getAsString(field.column);
+ if (!TextUtils.equals(value1, value2)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse the given {@link Bundle} into the given {@link RawContactDelta} state,
+ * assuming the extras defined through {@link Intents}.
+ */
+ public static void parseExtras(Context context, AccountType accountType, RawContactDelta state,
+ Bundle extras) {
+ if (extras == null || extras.size() == 0) {
+ // Bail early if no useful data
+ return;
+ }
+
+ parseStructuredNameExtra(context, accountType, state, extras);
+ parseStructuredPostalExtra(accountType, state, extras);
+
+ {
+ // Phone
+ final DataKind kind = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
+ parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
+ Phone.NUMBER);
+ parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
+ Phone.NUMBER);
+ }
+
+ {
+ // Email
+ final DataKind kind = accountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+ parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA);
+ parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
+ Email.DATA);
+ parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
+ Email.DATA);
+ }
+
+ {
+ // Im
+ final DataKind kind = accountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+ fixupLegacyImType(extras);
+ parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
+ }
+
+ // Organization
+ final boolean hasOrg = extras.containsKey(Insert.COMPANY)
+ || extras.containsKey(Insert.JOB_TITLE);
+ final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+ if (hasOrg && RawContactModifier.canInsert(state, kindOrg)) {
+ final ValuesDelta child = RawContactModifier.insertChild(state, kindOrg);
+
+ final String company = extras.getString(Insert.COMPANY);
+ if (ContactsUtils.isGraphic(company)) {
+ child.put(Organization.COMPANY, company);
+ }
+
+ final String title = extras.getString(Insert.JOB_TITLE);
+ if (ContactsUtils.isGraphic(title)) {
+ child.put(Organization.TITLE, title);
+ }
+ }
+
+ // Notes
+ final boolean hasNotes = extras.containsKey(Insert.NOTES);
+ final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
+ if (hasNotes && RawContactModifier.canInsert(state, kindNotes)) {
+ final ValuesDelta child = RawContactModifier.insertChild(state, kindNotes);
+
+ final String notes = extras.getString(Insert.NOTES);
+ if (ContactsUtils.isGraphic(notes)) {
+ child.put(Note.NOTE, notes);
+ }
+ }
+
+ // Arbitrary additional data
+ ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
+ if (values != null) {
+ parseValues(state, accountType, values);
+ }
+ }
+
+ private static void parseStructuredNameExtra(
+ Context context, AccountType accountType, RawContactDelta state, Bundle extras) {
+ // StructuredName
+ RawContactModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
+ final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
+
+ final String name = extras.getString(Insert.NAME);
+ if (ContactsUtils.isGraphic(name)) {
+ final DataKind kind = accountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+ boolean supportsDisplayName = false;
+ if (kind.fieldList != null) {
+ for (EditField field : kind.fieldList) {
+ if (StructuredName.DISPLAY_NAME.equals(field.column)) {
+ supportsDisplayName = true;
+ break;
+ }
+ }
+ }
+
+ if (supportsDisplayName) {
+ child.put(StructuredName.DISPLAY_NAME, name);
+ } else {
+ Uri uri = ContactsContract.AUTHORITY_URI.buildUpon()
+ .appendPath("complete_name")
+ .appendQueryParameter(StructuredName.DISPLAY_NAME, name)
+ .build();
+ Cursor cursor = context.getContentResolver().query(uri,
+ new String[]{
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX,
+ }, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ child.put(StructuredName.PREFIX, cursor.getString(0));
+ child.put(StructuredName.GIVEN_NAME, cursor.getString(1));
+ child.put(StructuredName.MIDDLE_NAME, cursor.getString(2));
+ child.put(StructuredName.FAMILY_NAME, cursor.getString(3));
+ child.put(StructuredName.SUFFIX, cursor.getString(4));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
+ if (ContactsUtils.isGraphic(phoneticName)) {
+ child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName);
+ }
+ }
+
+ private static void parseStructuredPostalExtra(
+ AccountType accountType, RawContactDelta state, Bundle extras) {
+ // StructuredPostal
+ final DataKind kind = accountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+ final ValuesDelta child = parseExtras(state, kind, extras, Insert.POSTAL_TYPE,
+ Insert.POSTAL, StructuredPostal.FORMATTED_ADDRESS);
+ String address = child == null ? null
+ : child.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ if (!TextUtils.isEmpty(address)) {
+ boolean supportsFormatted = false;
+ if (kind.fieldList != null) {
+ for (EditField field : kind.fieldList) {
+ if (StructuredPostal.FORMATTED_ADDRESS.equals(field.column)) {
+ supportsFormatted = true;
+ break;
+ }
+ }
+ }
+
+ if (!supportsFormatted) {
+ child.put(StructuredPostal.STREET, address);
+ child.putNull(StructuredPostal.FORMATTED_ADDRESS);
+ }
+ }
+ }
+
+ private static void parseValues(
+ RawContactDelta state, AccountType accountType,
+ ArrayList<ContentValues> dataValueList) {
+ for (ContentValues values : dataValueList) {
+ String mimeType = values.getAsString(Data.MIMETYPE);
+ if (TextUtils.isEmpty(mimeType)) {
+ Log.e(TAG, "Mimetype is required. Ignoring: " + values);
+ continue;
+ }
+
+ // Won't override the contact name
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ continue;
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ values.remove(PhoneDataItem.KEY_FORMATTED_PHONE_NUMBER);
+ final Integer type = values.getAsInteger(Phone.TYPE);
+ // If the provided phone number provides a custom phone type but not a label,
+ // replace it with mobile (by default) to avoid the "Enter custom label" from
+ // popping up immediately upon entering the ContactEditorFragment
+ if (type != null && type == Phone.TYPE_CUSTOM &&
+ TextUtils.isEmpty(values.getAsString(Phone.LABEL))) {
+ values.put(Phone.TYPE, Phone.TYPE_MOBILE);
+ }
+ }
+
+ DataKind kind = accountType.getKindForMimetype(mimeType);
+ if (kind == null) {
+ Log.e(TAG, "Mimetype not supported for account type "
+ + accountType.getAccountTypeAndDataSet() + ". Ignoring: " + values);
+ continue;
+ }
+
+ ValuesDelta entry = ValuesDelta.fromAfter(values);
+ if (isEmpty(entry, kind)) {
+ continue;
+ }
+
+ ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+
+ if ((kind.typeOverallMax != 1) || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Check for duplicates
+ boolean addEntry = true;
+ int count = 0;
+ if (entries != null && entries.size() > 0) {
+ for (ValuesDelta delta : entries) {
+ if (!delta.isDelete()) {
+ if (areEqual(delta, values, kind)) {
+ addEntry = false;
+ break;
+ }
+ count++;
+ }
+ }
+ }
+
+ if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) {
+ Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax
+ + " entries. Ignoring: " + values);
+ addEntry = false;
+ }
+
+ if (addEntry) {
+ addEntry = adjustType(entry, entries, kind);
+ }
+
+ if (addEntry) {
+ state.addEntry(entry);
+ }
+ } else {
+ // Non-list entries should not be overridden
+ boolean addEntry = true;
+ if (entries != null && entries.size() > 0) {
+ for (ValuesDelta delta : entries) {
+ if (!delta.isDelete() && !isEmpty(delta, kind)) {
+ addEntry = false;
+ break;
+ }
+ }
+ if (addEntry) {
+ for (ValuesDelta delta : entries) {
+ delta.markDeleted();
+ }
+ }
+ }
+
+ if (addEntry) {
+ addEntry = adjustType(entry, entries, kind);
+ }
+
+ if (addEntry) {
+ state.addEntry(entry);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){
+ // Note is most likely to contain large amounts of text
+ // that we don't want to drop on the ground.
+ for (ValuesDelta delta : entries) {
+ if (!isEmpty(delta, kind)) {
+ delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n"
+ + values.getAsString(Note.NOTE));
+ break;
+ }
+ }
+ } else {
+ Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: "
+ + values);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the data kind allows addition of another entry (e.g. Exchange only
+ * supports two "work" phone numbers). If not, tries to switch to one of the
+ * unused types. If successful, returns true.
+ */
+ private static boolean adjustType(
+ ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) {
+ if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) {
+ return true;
+ }
+
+ Integer typeInteger = entry.getAsInteger(kind.typeColumn);
+ int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue;
+
+ if (isTypeAllowed(type, entries, kind)) {
+ entry.put(kind.typeColumn, type);
+ return true;
+ }
+
+ // Specified type is not allowed - choose the first available type that is allowed
+ int size = kind.typeList.size();
+ for (int i = 0; i < size; i++) {
+ EditType editType = kind.typeList.get(i);
+ if (isTypeAllowed(editType.rawValue, entries, kind)) {
+ entry.put(kind.typeColumn, editType.rawValue);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if a new entry of the specified type can be added to the raw
+ * contact. For example, Exchange only supports two "work" phone numbers, so
+ * addition of a third would not be allowed.
+ */
+ private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) {
+ int max = 0;
+ int size = kind.typeList.size();
+ for (int i = 0; i < size; i++) {
+ EditType editType = kind.typeList.get(i);
+ if (editType.rawValue == type) {
+ max = editType.specificMax;
+ break;
+ }
+ }
+
+ if (max == 0) {
+ // This type is not allowed at all
+ return false;
+ }
+
+ if (max == -1) {
+ // Unlimited instances of this type are allowed
+ return true;
+ }
+
+ return getEntryCountByType(entries, kind.typeColumn, type) < max;
+ }
+
+ /**
+ * Counts occurrences of the specified type in the supplied entry list.
+ *
+ * @return The count of occurrences of the type in the entry list. 0 if entries is
+ * {@literal null}
+ */
+ private static int getEntryCountByType(ArrayList<ValuesDelta> entries, String typeColumn,
+ int type) {
+ int count = 0;
+ if (entries != null) {
+ for (ValuesDelta entry : entries) {
+ Integer typeInteger = entry.getAsInteger(typeColumn);
+ if (typeInteger != null && typeInteger == type) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
+ * with updated values.
+ */
+ @SuppressWarnings("deprecation")
+ private static void fixupLegacyImType(Bundle bundle) {
+ final String encodedString = bundle.getString(Insert.IM_PROTOCOL);
+ if (encodedString == null) return;
+
+ try {
+ final Object protocol = android.provider.Contacts.ContactMethods
+ .decodeImProtocol(encodedString);
+ if (protocol instanceof Integer) {
+ bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol);
+ } else {
+ bundle.putString(Insert.IM_PROTOCOL, (String)protocol);
+ }
+ } catch (IllegalArgumentException e) {
+ // Ignore exception when legacy parser fails
+ }
+ }
+
+ /**
+ * Parse a specific entry from the given {@link Bundle} and insert into the
+ * given {@link RawContactDelta}. Silently skips the insert when missing value
+ * or no valid {@link EditType} found.
+ *
+ * @param typeExtra {@link Bundle} key that holds the incoming
+ * {@link EditType#rawValue} value.
+ * @param valueExtra {@link Bundle} key that holds the incoming value.
+ * @param valueColumn Column to write value into {@link ValuesDelta}.
+ */
+ public static ValuesDelta parseExtras(RawContactDelta state, DataKind kind, Bundle extras,
+ String typeExtra, String valueExtra, String valueColumn) {
+ final CharSequence value = extras.getCharSequence(valueExtra);
+
+ // Bail early if account type doesn't handle this MIME type
+ if (kind == null) return null;
+
+ // Bail when can't insert type, or value missing
+ final boolean canInsert = RawContactModifier.canInsert(state, kind);
+ final boolean validValue = (value != null && TextUtils.isGraphic(value));
+ if (!validValue || !canInsert) return null;
+
+ // Find exact type when requested, otherwise best available type
+ final boolean hasType = extras.containsKey(typeExtra);
+ final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
+ : Integer.MIN_VALUE);
+ final EditType editType = RawContactModifier.getBestValidType(state, kind, true, typeValue);
+
+ // Create data row and fill with value
+ final ValuesDelta child = RawContactModifier.insertChild(state, kind, editType);
+ child.put(valueColumn, value.toString());
+
+ if (editType != null && editType.customColumn != null) {
+ // Write down label when custom type picked
+ final String customType = extras.getString(typeExtra);
+ child.put(editType.customColumn, customType);
+ }
+
+ return child;
+ }
+
+ /**
+ * Generic mime types with type support (e.g. TYPE_HOME).
+ * Here, "type support" means if the data kind has CommonColumns#TYPE or not. Data kinds which
+ * have their own migrate methods aren't listed here.
+ */
+ private static final Set<String> sGenericMimeTypesWithTypeSupport = new HashSet<String>(
+ Arrays.asList(Phone.CONTENT_ITEM_TYPE,
+ Email.CONTENT_ITEM_TYPE,
+ Im.CONTENT_ITEM_TYPE,
+ Nickname.CONTENT_ITEM_TYPE,
+ Website.CONTENT_ITEM_TYPE,
+ Relation.CONTENT_ITEM_TYPE,
+ SipAddress.CONTENT_ITEM_TYPE));
+ private static final Set<String> sGenericMimeTypesWithoutTypeSupport = new HashSet<String>(
+ Arrays.asList(Organization.CONTENT_ITEM_TYPE,
+ Note.CONTENT_ITEM_TYPE,
+ Photo.CONTENT_ITEM_TYPE,
+ GroupMembership.CONTENT_ITEM_TYPE));
+ // CommonColumns.TYPE cannot be accessed as it is protected interface, so use
+ // Phone.TYPE instead.
+ private static final String COLUMN_FOR_TYPE = Phone.TYPE;
+ private static final String COLUMN_FOR_LABEL = Phone.LABEL;
+ private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM;
+
+ /**
+ * Migrates old RawContactDelta to newly created one with a new restriction supplied from
+ * newAccountType.
+ *
+ * This is only for account switch during account creation (which must be insert operation).
+ */
+ public static void migrateStateForNewContact(Context context,
+ RawContactDelta oldState, RawContactDelta newState,
+ AccountType oldAccountType, AccountType newAccountType) {
+ if (newAccountType == oldAccountType) {
+ // Just copying all data in oldState isn't enough, but we can still rely on a lot of
+ // shortcuts.
+ for (DataKind kind : newAccountType.getSortedDataKinds()) {
+ final String mimeType = kind.mimeType;
+ // The fields with short/long form capability must be treated properly.
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ migrateStructuredName(context, oldState, newState, kind);
+ } else {
+ List<ValuesDelta> entryList = oldState.getMimeEntries(mimeType);
+ if (entryList != null && !entryList.isEmpty()) {
+ for (ValuesDelta entry : entryList) {
+ ContentValues values = entry.getAfter();
+ if (values != null) {
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // Migrate data supported by the new account type.
+ // All the other data inside oldState are silently dropped.
+ for (DataKind kind : newAccountType.getSortedDataKinds()) {
+ if (!kind.editable) continue;
+ final String mimeType = kind.mimeType;
+ if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
+ || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
+ // Ignore pseudo data.
+ continue;
+ } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ migrateStructuredName(context, oldState, newState, kind);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ migratePostal(oldState, newState, kind);
+ } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ migrateEvent(oldState, newState, kind, null /* default Year */);
+ } else if (sGenericMimeTypesWithoutTypeSupport.contains(mimeType)) {
+ migrateGenericWithoutTypeColumn(oldState, newState, kind);
+ } else if (sGenericMimeTypesWithTypeSupport.contains(mimeType)) {
+ migrateGenericWithTypeColumn(oldState, newState, kind);
+ } else {
+ throw new IllegalStateException("Unexpected editable mime-type: " + mimeType);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts
+ * the number of entries (ValuesDelta) inside newState.
+ */
+ private static ArrayList<ValuesDelta> ensureEntryMaxSize(RawContactDelta newState,
+ DataKind kind, ArrayList<ValuesDelta> mimeEntries) {
+ if (mimeEntries == null) {
+ return null;
+ }
+
+ final int typeOverallMax = kind.typeOverallMax;
+ if (typeOverallMax >= 0 && (mimeEntries.size() > typeOverallMax)) {
+ ArrayList<ValuesDelta> newMimeEntries = new ArrayList<ValuesDelta>(typeOverallMax);
+ for (int i = 0; i < typeOverallMax; i++) {
+ newMimeEntries.add(mimeEntries.get(i));
+ }
+ mimeEntries = newMimeEntries;
+ }
+ return mimeEntries;
+ }
+
+ /** @hide Public only for testing. */
+ public static void migrateStructuredName(
+ Context context, RawContactDelta oldState, RawContactDelta newState,
+ DataKind newDataKind) {
+ final ContentValues values =
+ oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter();
+ if (values == null) {
+ return;
+ }
+
+ boolean supportDisplayName = false;
+ boolean supportPhoneticFullName = false;
+ boolean supportPhoneticFamilyName = false;
+ boolean supportPhoneticMiddleName = false;
+ boolean supportPhoneticGivenName = false;
+ for (EditField editField : newDataKind.fieldList) {
+ if (StructuredName.DISPLAY_NAME.equals(editField.column)) {
+ supportDisplayName = true;
+ }
+ if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) {
+ supportPhoneticFullName = true;
+ }
+ if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) {
+ supportPhoneticFamilyName = true;
+ }
+ if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) {
+ supportPhoneticMiddleName = true;
+ }
+ if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) {
+ supportPhoneticGivenName = true;
+ }
+ }
+
+ // DISPLAY_NAME <-> PREFIX, GIVEN_NAME, MIDDLE_NAME, FAMILY_NAME, SUFFIX
+ final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+ if (!TextUtils.isEmpty(displayName)) {
+ if (!supportDisplayName) {
+ // Old data has a display name, while the new account doesn't allow it.
+ NameConverter.displayNameToStructuredName(context, displayName, values);
+
+ // We don't want to migrate unseen data which may confuse users after the creation.
+ values.remove(StructuredName.DISPLAY_NAME);
+ }
+ } else {
+ if (supportDisplayName) {
+ // Old data does not have display name, while the new account requires it.
+ values.put(StructuredName.DISPLAY_NAME,
+ NameConverter.structuredNameToDisplayName(context, values));
+ for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+ values.remove(field);
+ }
+ }
+ }
+
+ // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME
+ final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+ if (!TextUtils.isEmpty(phoneticFullName)) {
+ if (!supportPhoneticFullName) {
+ // Old data has a phonetic (full) name, while the new account doesn't allow it.
+ final StructuredNameDataItem tmpItem =
+ NameConverter.parsePhoneticName(phoneticFullName, null);
+ values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+ if (supportPhoneticFamilyName) {
+ values.put(StructuredName.PHONETIC_FAMILY_NAME,
+ tmpItem.getPhoneticFamilyName());
+ } else {
+ values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+ if (supportPhoneticMiddleName) {
+ values.put(StructuredName.PHONETIC_MIDDLE_NAME,
+ tmpItem.getPhoneticMiddleName());
+ } else {
+ values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+ if (supportPhoneticGivenName) {
+ values.put(StructuredName.PHONETIC_GIVEN_NAME,
+ tmpItem.getPhoneticGivenName());
+ } else {
+ values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+ }
+ } else {
+ if (supportPhoneticFullName) {
+ // Old data does not have a phonetic (full) name, while the new account requires it.
+ values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+ NameConverter.buildPhoneticName(
+ values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
+ values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
+ values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)));
+ }
+ if (!supportPhoneticFamilyName) {
+ values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+ if (!supportPhoneticMiddleName) {
+ values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+ if (!supportPhoneticGivenName) {
+ values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+ }
+
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ }
+
+ /** @hide Public only for testing. */
+ public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
+ DataKind newDataKind) {
+ final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+ oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
+ if (mimeEntries == null || mimeEntries.isEmpty()) {
+ return;
+ }
+
+ boolean supportFormattedAddress = false;
+ boolean supportStreet = false;
+ final String firstColumn = newDataKind.fieldList.get(0).column;
+ for (EditField editField : newDataKind.fieldList) {
+ if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) {
+ supportFormattedAddress = true;
+ }
+ if (StructuredPostal.STREET.equals(editField.column)) {
+ supportStreet = true;
+ }
+ }
+
+ final Set<Integer> supportedTypes = new HashSet<Integer>();
+ if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+ for (EditType editType : newDataKind.typeList) {
+ supportedTypes.add(editType.rawValue);
+ }
+ }
+
+ for (ValuesDelta entry : mimeEntries) {
+ final ContentValues values = entry.getAfter();
+ if (values == null) {
+ continue;
+ }
+ final Integer oldType = values.getAsInteger(StructuredPostal.TYPE);
+ if (!supportedTypes.contains(oldType)) {
+ int defaultType;
+ if (newDataKind.defaultValues != null) {
+ defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE);
+ } else {
+ defaultType = newDataKind.typeList.get(0).rawValue;
+ }
+ values.put(StructuredPostal.TYPE, defaultType);
+ if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) {
+ values.remove(StructuredPostal.LABEL);
+ }
+ }
+
+ final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ if (!TextUtils.isEmpty(formattedAddress)) {
+ if (!supportFormattedAddress) {
+ // Old data has a formatted address, while the new account doesn't allow it.
+ values.remove(StructuredPostal.FORMATTED_ADDRESS);
+
+ // Unlike StructuredName we don't have logic to split it, so first
+ // try to use street field and. If the new account doesn't have one,
+ // then select first one anyway.
+ if (supportStreet) {
+ values.put(StructuredPostal.STREET, formattedAddress);
+ } else {
+ values.put(firstColumn, formattedAddress);
+ }
+ }
+ } else {
+ if (supportFormattedAddress) {
+ // Old data does not have formatted address, while the new account requires it.
+ // Unlike StructuredName we don't have logic to join multiple address values.
+ // Use poor join heuristics for now.
+ String[] structuredData;
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ if (useJapaneseOrder) {
+ structuredData = new String[] {
+ values.getAsString(StructuredPostal.COUNTRY),
+ values.getAsString(StructuredPostal.POSTCODE),
+ values.getAsString(StructuredPostal.REGION),
+ values.getAsString(StructuredPostal.CITY),
+ values.getAsString(StructuredPostal.NEIGHBORHOOD),
+ values.getAsString(StructuredPostal.STREET),
+ values.getAsString(StructuredPostal.POBOX) };
+ } else {
+ structuredData = new String[] {
+ values.getAsString(StructuredPostal.POBOX),
+ values.getAsString(StructuredPostal.STREET),
+ values.getAsString(StructuredPostal.NEIGHBORHOOD),
+ values.getAsString(StructuredPostal.CITY),
+ values.getAsString(StructuredPostal.REGION),
+ values.getAsString(StructuredPostal.POSTCODE),
+ values.getAsString(StructuredPostal.COUNTRY) };
+ }
+ final StringBuilder builder = new StringBuilder();
+ for (String elem : structuredData) {
+ if (!TextUtils.isEmpty(elem)) {
+ builder.append(elem + "\n");
+ }
+ }
+ values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString());
+
+ values.remove(StructuredPostal.POBOX);
+ values.remove(StructuredPostal.STREET);
+ values.remove(StructuredPostal.NEIGHBORHOOD);
+ values.remove(StructuredPostal.CITY);
+ values.remove(StructuredPostal.REGION);
+ values.remove(StructuredPostal.POSTCODE);
+ values.remove(StructuredPostal.COUNTRY);
+ }
+ }
+
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ }
+ }
+
+ /** @hide Public only for testing. */
+ public static void migrateEvent(RawContactDelta oldState, RawContactDelta newState,
+ DataKind newDataKind, Integer defaultYear) {
+ final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+ oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE));
+ if (mimeEntries == null || mimeEntries.isEmpty()) {
+ return;
+ }
+
+ final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>();
+ for (EditType editType : newDataKind.typeList) {
+ allowedTypes.put(editType.rawValue, (EventEditType) editType);
+ }
+ for (ValuesDelta entry : mimeEntries) {
+ final ContentValues values = entry.getAfter();
+ if (values == null) {
+ continue;
+ }
+ final String dateString = values.getAsString(Event.START_DATE);
+ final Integer type = values.getAsInteger(Event.TYPE);
+ if (type != null && (allowedTypes.indexOfKey(type) >= 0)
+ && !TextUtils.isEmpty(dateString)) {
+ EventEditType suitableType = allowedTypes.get(type);
+
+ final ParsePosition position = new ParsePosition(0);
+ boolean yearOptional = false;
+ Date date = CommonDateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position);
+ if (date == null) {
+ yearOptional = true;
+ date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position);
+ }
+ if (date != null) {
+ if (yearOptional && !suitableType.isYearOptional()) {
+ // The new EditType doesn't allow optional year. Supply default.
+ final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE,
+ Locale.US);
+ if (defaultYear == null) {
+ defaultYear = calendar.get(Calendar.YEAR);
+ }
+ calendar.setTime(date);
+ final int month = calendar.get(Calendar.MONTH);
+ final int day = calendar.get(Calendar.DAY_OF_MONTH);
+ // Exchange requires 8:00 for birthdays
+ calendar.set(defaultYear, month, day,
+ CommonDateUtils.DEFAULT_HOUR, 0, 0);
+ values.put(Event.START_DATE,
+ CommonDateUtils.FULL_DATE_FORMAT.format(calendar.getTime()));
+ }
+ }
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ } else {
+ // Just drop it.
+ }
+ }
+ }
+
+ /** @hide Public only for testing. */
+ public static void migrateGenericWithoutTypeColumn(
+ RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+ final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+ oldState.getMimeEntries(newDataKind.mimeType));
+ if (mimeEntries == null || mimeEntries.isEmpty()) {
+ return;
+ }
+
+ for (ValuesDelta entry : mimeEntries) {
+ ContentValues values = entry.getAfter();
+ if (values != null) {
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ }
+ }
+ }
+
+ /** @hide Public only for testing. */
+ public static void migrateGenericWithTypeColumn(
+ RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+ final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
+ if (mimeEntries == null || mimeEntries.isEmpty()) {
+ return;
+ }
+
+ // Note that type specified with the old account may be invalid with the new account, while
+ // we want to preserve its data as much as possible. e.g. if a user typed a phone number
+ // with a type which is valid with an old account but not with a new account, the user
+ // probably wants to have the number with default type, rather than seeing complete data
+ // loss.
+ //
+ // Specifically, this method works as follows:
+ // 1. detect defaultType
+ // 2. prepare constants & variables for iteration
+ // 3. iterate over mimeEntries:
+ // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in
+ // DataKind
+ // 3.2 replace unallowed types with defaultType
+ // 3.3 check if the number of entries is below specificMax specified in AccountType
+
+ // Here, defaultType can be supplied in two ways
+ // - via kind.defaultValues
+ // - via kind.typeList.get(0).rawValue
+ Integer defaultType = null;
+ if (newDataKind.defaultValues != null) {
+ defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE);
+ }
+ final Set<Integer> allowedTypes = new HashSet<Integer>();
+ // key: type, value: the number of entries allowed for the type (specificMax)
+ final SparseIntArray typeSpecificMaxMap = new SparseIntArray();
+ if (defaultType != null) {
+ allowedTypes.add(defaultType);
+ typeSpecificMaxMap.put(defaultType, -1);
+ }
+ // Note: typeList may be used in different purposes when defaultValues are specified.
+ // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK)
+ // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add
+ // anything other than defaultType into allowedTypes and typeSpecificMapMax.
+ if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) &&
+ newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+ for (EditType editType : newDataKind.typeList) {
+ allowedTypes.add(editType.rawValue);
+ typeSpecificMaxMap.put(editType.rawValue, editType.specificMax);
+ }
+ if (defaultType == null) {
+ defaultType = newDataKind.typeList.get(0).rawValue;
+ }
+ }
+
+ if (defaultType == null) {
+ Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType);
+ }
+
+ final int typeOverallMax = newDataKind.typeOverallMax;
+
+ // key: type, value: the number of current entries.
+ final SparseIntArray currentEntryCount = new SparseIntArray();
+ int totalCount = 0;
+
+ for (ValuesDelta entry : mimeEntries) {
+ if (typeOverallMax != -1 && totalCount >= typeOverallMax) {
+ break;
+ }
+
+ final ContentValues values = entry.getAfter();
+ if (values == null) {
+ continue;
+ }
+
+ final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE);
+ final Integer typeForNewAccount;
+ if (!allowedTypes.contains(oldType)) {
+ // The new account doesn't support the type.
+ if (defaultType != null) {
+ typeForNewAccount = defaultType.intValue();
+ values.put(COLUMN_FOR_TYPE, defaultType.intValue());
+ if (oldType != null && oldType == TYPE_CUSTOM) {
+ values.remove(COLUMN_FOR_LABEL);
+ }
+ } else {
+ typeForNewAccount = null;
+ values.remove(COLUMN_FOR_TYPE);
+ }
+ } else {
+ typeForNewAccount = oldType;
+ }
+ if (typeForNewAccount != null) {
+ final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0);
+ if (specificMax >= 0) {
+ final int currentCount = currentEntryCount.get(typeForNewAccount, 0);
+ if (currentCount >= specificMax) {
+ continue;
+ }
+ currentEntryCount.put(typeForNewAccount, currentCount + 1);
+ }
+ }
+ newState.addEntry(ValuesDelta.fromAfter(values));
+ totalCount++;
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/model/account/ExternalAccountType.java b/src/com/android/contacts/common/model/account/ExternalAccountType.java
index 5697efe3..f6ab375d 100644
--- a/src/com/android/contacts/common/model/account/ExternalAccountType.java
+++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -366,17 +366,16 @@ public class ExternalAccountType extends BaseAccountType {
final DataKind kind = new DataKind();
kind.mimeType = a
- .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType);
-
+ .getString(android.R.styleable.ContactsDataKind_mimeType);
final String summaryColumn = a.getString(
- com.android.internal.R.styleable.ContactsDataKind_summaryColumn);
+ android.R.styleable.ContactsDataKind_summaryColumn);
if (summaryColumn != null) {
// Inflate a specific column as summary when requested
kind.actionHeader = new SimpleInflater(summaryColumn);
}
final String detailColumn = a.getString(
- com.android.internal.R.styleable.ContactsDataKind_detailColumn);
+ android.R.styleable.ContactsDataKind_detailColumn);
if (detailColumn != null) {
// Inflate specific column as summary
diff --git a/src/com/android/contacts/common/model/dataitem/DataItem.java b/src/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 00000000..60a006f8
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+
+/**
+ * This is the base class for data items, which represents a row from the Data table.
+ */
+public class DataItem {
+
+ private final ContentValues mContentValues;
+
+ protected DataItem(ContentValues values) {
+ mContentValues = values;
+ }
+
+ /**
+ * Factory for creating subclasses of DataItem objects based on the mimetype in the
+ * content values. Raw contact is the raw contact that this data item is associated with.
+ */
+ public static DataItem createFrom(ContentValues values) {
+ final String mimeType = values.getAsString(Data.MIMETYPE);
+ if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new GroupMembershipDataItem(values);
+ } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredNameDataItem(values);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhoneDataItem(values);
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EmailDataItem(values);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredPostalDataItem(values);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new ImDataItem(values);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new OrganizationDataItem(values);
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NicknameDataItem(values);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NoteDataItem(values);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new WebsiteDataItem(values);
+ } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new SipAddressDataItem(values);
+ } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EventDataItem(values);
+ } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new RelationDataItem(values);
+ } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new IdentityDataItem(values);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhotoDataItem(values);
+ }
+
+ // generic
+ return new DataItem(values);
+ }
+
+ public ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public void setRawContactId(long rawContactId) {
+ mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ }
+
+ /**
+ * Returns the data id.
+ */
+ public long getId() {
+ return mContentValues.getAsLong(Data._ID);
+ }
+
+ /**
+ * Returns the mimetype of the data.
+ */
+ public String getMimeType() {
+ return mContentValues.getAsString(Data.MIMETYPE);
+ }
+
+ public void setMimeType(String mimeType) {
+ mContentValues.put(Data.MIMETYPE, mimeType);
+ }
+
+ public boolean isPrimary() {
+ Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+ return primary != null && primary != 0;
+ }
+
+ public boolean isSuperPrimary() {
+ Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+ return superPrimary != null && superPrimary != 0;
+ }
+
+ public boolean hasKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return key != null && mContentValues.containsKey(key) &&
+ mContentValues.getAsInteger(key) != null;
+ }
+
+ public int getKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return mContentValues.getAsInteger(key);
+ }
+
+ /**
+ * This builds the data string depending on the type of data item by using the generic
+ * DataKind object underneath.
+ */
+ public String buildDataString(Context context, DataKind kind) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * This builds the data string(intended for display) depending on the type of data item. It
+ * returns the same value as {@link #buildDataString} by default, but certain data items can
+ * override it to provide their version of formatted data strings.
+ *
+ * @return Data string representing the data item, possibly formatted for display
+ */
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ return buildDataString(context, kind);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/EmailDataItem.java b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 00000000..23efb015
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+ /* package */ EmailDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getAddress() {
+ return getContentValues().getAsString(Email.ADDRESS);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(Email.DISPLAY_NAME);
+ }
+
+ public String getData() {
+ return getContentValues().getAsString(Email.DATA);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Email.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/EventDataItem.java b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 00000000..e664db18
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+
+/**
+ * Represents an event data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+ /* package */ EventDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getStartDate() {
+ return getContentValues().getAsString(Event.START_DATE);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Event.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 00000000..41f19e65
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+ /* package */ GroupMembershipDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getGroupRowId() {
+ return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+ }
+
+ public String getGroupSourceId() {
+ return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 00000000..29e9a401
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+ /* package */ IdentityDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getIdentity() {
+ return getContentValues().getAsString(Identity.IDENTITY);
+ }
+
+ public String getNamespace() {
+ return getContentValues().getAsString(Identity.NAMESPACE);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/ImDataItem.java b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 00000000..532b89f1
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+
+/**
+ * Represents an IM data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+ private final boolean mCreatedFromEmail;
+
+ /* package */ ImDataItem(ContentValues values) {
+ super(values);
+ mCreatedFromEmail = false;
+ }
+
+ private ImDataItem(ContentValues values, boolean createdFromEmail) {
+ super(values);
+ mCreatedFromEmail = createdFromEmail;
+ }
+
+ public static ImDataItem createFromEmail(EmailDataItem item) {
+ ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true);
+ im.setMimeType(Im.CONTENT_ITEM_TYPE);
+ return im;
+ }
+
+ public String getData() {
+ if (mCreatedFromEmail) {
+ return getContentValues().getAsString(Email.DATA);
+ } else {
+ return getContentValues().getAsString(Im.DATA);
+ }
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Im.LABEL);
+ }
+
+ /**
+ * Values are one of Im.PROTOCOL_
+ */
+ public Integer getProtocol() {
+ return getContentValues().getAsInteger(Im.PROTOCOL);
+ }
+
+ public boolean isProtocolValid() {
+ return getProtocol() != null;
+ }
+
+ public String getCustomProtocol() {
+ return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+ }
+
+ public int getChatCapability() {
+ Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+ return result == null ? 0 : result;
+ }
+
+ public boolean isCreatedFromEmail() {
+ return mCreatedFromEmail;
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 00000000..e7f9d4a5
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+ public NicknameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Nickname.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Nickname.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/NoteDataItem.java b/src/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 00000000..3d711673
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NoteDataItem.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+ /* package */ NoteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNote() {
+ return getContentValues().getAsString(Note.NOTE);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 00000000..9f4b8d3e
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+ /* package */ OrganizationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getCompany() {
+ return getContentValues().getAsString(Organization.COMPANY);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Organization.LABEL);
+ }
+
+ public String getTitle() {
+ return getContentValues().getAsString(Organization.TITLE);
+ }
+
+ public String getDepartment() {
+ return getContentValues().getAsString(Organization.DEPARTMENT);
+ }
+
+ public String getJobDescription() {
+ return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+ }
+
+ public String getSymbol() {
+ return getContentValues().getAsString(Organization.SYMBOL);
+ }
+
+ public String getPhoneticName() {
+ return getContentValues().getAsString(Organization.PHONETIC_NAME);
+ }
+
+ public String getOfficeLocation() {
+ return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+ }
+
+ public String getPhoneticNameStyle() {
+ return getContentValues().getAsString(Organization.PHONETIC_NAME_STYLE);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 00000000..f45e025d
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+
+/**
+ * Represents a phone data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+ public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+ /* package */ PhoneDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNumber() {
+ return getContentValues().getAsString(Phone.NUMBER);
+ }
+
+ /**
+ * Returns the normalized phone number in E164 format.
+ */
+ public String getNormalizedNumber() {
+ return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+ }
+
+ public String getFormattedPhoneNumber() {
+ return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Phone.LABEL);
+ }
+
+ public void computeFormattedPhoneNumber(String defaultCountryIso) {
+ final String phoneNumber = getNumber();
+ if (phoneNumber != null) {
+ final String formattedPhoneNumber = PhoneNumberUtils.formatNumber(phoneNumber,
+ getNormalizedNumber(), defaultCountryIso);
+ getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+ }
+ }
+
+ /**
+ * Returns the formatted phone number (if already computed using {@link
+ * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+ */
+ @Override
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ final String formatted = getFormattedPhoneNumber();
+ if (formatted != null) {
+ return formatted;
+ } else {
+ return getNumber();
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 00000000..a61218b2
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in
+ * {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+ /* package */ PhotoDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getPhotoFileId() {
+ return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+ }
+
+ public byte[] getPhoto() {
+ return getContentValues().getAsByteArray(Photo.PHOTO);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/RelationDataItem.java b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 00000000..b6992978
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+
+/**
+ * Represents a relation data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+ /* package */ RelationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Relation.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Relation.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 00000000..ec704fc3
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+ /* package */ SipAddressDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getSipAddress() {
+ return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(SipAddress.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 00000000..ce2c84a9
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+ public StructuredNameDataItem() {
+ super(new ContentValues());
+ getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ }
+
+ /* package */ StructuredNameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+ }
+
+ public void setDisplayName(String name) {
+ getContentValues().put(StructuredName.DISPLAY_NAME, name);
+ }
+
+ public String getGivenName() {
+ return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+ }
+
+ public String getFamilyName() {
+ return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+ }
+
+ public String getPrefix() {
+ return getContentValues().getAsString(StructuredName.PREFIX);
+ }
+
+ public String getMiddleName() {
+ return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+ }
+
+ public String getSuffix() {
+ return getContentValues().getAsString(StructuredName.SUFFIX);
+ }
+
+ public String getPhoneticGivenName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+
+ public String getPhoneticMiddleName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+
+ public String getPhoneticFamilyName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+
+ public String getFullNameStyle() {
+ return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+ }
+
+ public String getPhoneticNameStyle() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_NAME_STYLE);
+ }
+
+ public void setPhoneticFamilyName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+ }
+
+ public void setPhoneticMiddleName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+ }
+
+ public void setPhoneticGivenName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 00000000..6cfc0c16
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+ /* package */ StructuredPostalDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getFormattedAddress() {
+ return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(StructuredPostal.LABEL);
+ }
+
+ public String getStreet() {
+ return getContentValues().getAsString(StructuredPostal.STREET);
+ }
+
+ public String getPOBox() {
+ return getContentValues().getAsString(StructuredPostal.POBOX);
+ }
+
+ public String getNeighborhood() {
+ return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+ }
+
+ public String getCity() {
+ return getContentValues().getAsString(StructuredPostal.CITY);
+ }
+
+ public String getRegion() {
+ return getContentValues().getAsString(StructuredPostal.REGION);
+ }
+
+ public String getPostcode() {
+ return getContentValues().getAsString(StructuredPostal.POSTCODE);
+ }
+
+ public String getCountry() {
+ return getContentValues().getAsString(StructuredPostal.COUNTRY);
+ }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 00000000..0939421e
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+ /* package */ WebsiteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getUrl() {
+ return getContentValues().getAsString(Website.URL);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Website.LABEL);
+ }
+}
diff --git a/src/com/android/contacts/common/test/InjectedServices.java b/src/com/android/contacts/common/test/InjectedServices.java
new file mode 100644
index 00000000..75ad9380
--- /dev/null
+++ b/src/com/android/contacts/common/test/InjectedServices.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.test;
+
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+
+/**
+ * A mechanism for providing alternative (mock) services to the application
+ * while running tests. Activities, Services and the Application should check
+ * with this class to see if a particular service has been overridden.
+ */
+public class InjectedServices {
+
+ private ContentResolver mContentResolver;
+ private SharedPreferences mSharedPreferences;
+ private HashMap<String, Object> mSystemServices;
+
+ @VisibleForTesting
+ public void setContentResolver(ContentResolver contentResolver) {
+ this.mContentResolver = contentResolver;
+ }
+
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @VisibleForTesting
+ public void setSharedPreferences(SharedPreferences sharedPreferences) {
+ this.mSharedPreferences = sharedPreferences;
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ return mSharedPreferences;
+ }
+
+ @VisibleForTesting
+ public void setSystemService(String name, Object service) {
+ if (mSystemServices == null) {
+ mSystemServices = Maps.newHashMap();
+ }
+
+ mSystemServices.put(name, service);
+ }
+
+ public Object getSystemService(String name) {
+ if (mSystemServices != null) {
+ return mSystemServices.get(name);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/contacts/common/util/CommonDateUtils.java b/src/com/android/contacts/common/util/CommonDateUtils.java
index 5dfd149a..bba910ac 100644
--- a/src/com/android/contacts/common/util/CommonDateUtils.java
+++ b/src/com/android/contacts/common/util/CommonDateUtils.java
@@ -33,4 +33,9 @@ public class CommonDateUtils {
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+
+ /**
+ * Exchange requires 8:00 for birthdays
+ */
+ public final static int DEFAULT_HOUR = 8;
}
diff --git a/src/com/android/contacts/common/util/ContactLoaderUtils.java b/src/com/android/contacts/common/util/ContactLoaderUtils.java
new file mode 100644
index 00000000..0ec8887a
--- /dev/null
+++ b/src/com/android/contacts/common/util/ContactLoaderUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.contacts.common.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * Utility methods for the {@link ContactLoader}.
+ */
+public final class ContactLoaderUtils {
+
+ /** Static helper, not instantiable. */
+ private ContactLoaderUtils() {}
+
+ /**
+ * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+ * For legacy contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException}
+ * can be thrown if the URI is null or the authority is not recognized.
+ *
+ * Do not call from the UI thread.
+ */
+ @SuppressWarnings("deprecation")
+ public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri)
+ throws IllegalArgumentException {
+ if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+ final String authority = uri.getAuthority();
+
+ // Current Style Uri?
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ final String type = resolver.getType(uri);
+ // Contact-Uri? Good, return it
+ if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ return uri;
+ }
+
+ // RawContact-Uri? Transform it to ContactUri
+ if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(resolver,
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ // Anything else? We don't know what this is
+ throw new IllegalArgumentException("uri format is unknown");
+ }
+
+ // Legacy Style? Convert to RawContact
+ final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY;
+ if (OBSOLETE_AUTHORITY.equals(authority)) {
+ // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(resolver,
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ throw new IllegalArgumentException("uri authority is unknown");
+ }
+}
diff --git a/src/com/android/contacts/common/util/DataStatus.java b/src/com/android/contacts/common/util/DataStatus.java
new file mode 100644
index 00000000..76f11b65
--- /dev/null
+++ b/src/com/android/contacts/common/util/DataStatus.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Data;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.android.contacts.common.R;
+
+/**
+ * Storage for a social status update. Holds a single update, but can use
+ * {@link #possibleUpdate(Cursor)} to consider updating when a better status
+ * exists. Statuses with timestamps, or with newer timestamps win.
+ */
+public class DataStatus {
+ private int mPresence = -1;
+ private String mStatus = null;
+ private long mTimestamp = -1;
+
+ private String mResPackage = null;
+ private int mIconRes = -1;
+ private int mLabelRes = -1;
+
+ public DataStatus() {
+ }
+
+ public DataStatus(Cursor cursor) {
+ // When creating from cursor row, fill normally
+ fromCursor(cursor);
+ }
+
+ /**
+ * Attempt updating this {@link DataStatus} based on values at the
+ * current row of the given {@link Cursor}.
+ */
+ public void possibleUpdate(Cursor cursor) {
+ final boolean hasStatus = !isNull(cursor, Data.STATUS);
+ final boolean hasTimestamp = !isNull(cursor, Data.STATUS_TIMESTAMP);
+
+ // Bail early when not valid status, or when previous status was
+ // found and we can't compare this one.
+ if (!hasStatus) return;
+ if (isValid() && !hasTimestamp) return;
+
+ if (hasTimestamp) {
+ // Compare timestamps and bail if older status
+ final long newTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+ if (newTimestamp < mTimestamp) return;
+
+ mTimestamp = newTimestamp;
+ }
+
+ // Fill in remaining details from cursor
+ fromCursor(cursor);
+ }
+
+ private void fromCursor(Cursor cursor) {
+ mPresence = getInt(cursor, Data.PRESENCE, -1);
+ mStatus = getString(cursor, Data.STATUS);
+ mTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+ mResPackage = getString(cursor, Data.STATUS_RES_PACKAGE);
+ mIconRes = getInt(cursor, Data.STATUS_ICON, -1);
+ mLabelRes = getInt(cursor, Data.STATUS_LABEL, -1);
+ }
+
+ public boolean isValid() {
+ return !TextUtils.isEmpty(mStatus);
+ }
+
+ public int getPresence() {
+ return mPresence;
+ }
+
+ public CharSequence getStatus() {
+ return mStatus;
+ }
+
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * Build any timestamp and label into a single string.
+ */
+ public CharSequence getTimestampLabel(Context context) {
+ final PackageManager pm = context.getPackageManager();
+
+ // Use local package for resources when none requested
+ if (mResPackage == null) mResPackage = context.getPackageName();
+
+ final boolean validTimestamp = mTimestamp > 0;
+ final boolean validLabel = mResPackage != null && mLabelRes != -1;
+
+ final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
+ mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE) : null;
+ final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
+ null) : null;
+
+ if (validTimestamp && validLabel) {
+ return context.getString(
+ R.string.contact_status_update_attribution_with_date,
+ timeClause, labelClause);
+ } else if (validLabel) {
+ return context.getString(
+ R.string.contact_status_update_attribution,
+ labelClause);
+ } else if (validTimestamp) {
+ return timeClause;
+ } else {
+ return null;
+ }
+ }
+
+ public Drawable getIcon(Context context) {
+ final PackageManager pm = context.getPackageManager();
+
+ // Use local package for resources when none requested
+ if (mResPackage == null) mResPackage = context.getPackageName();
+
+ final boolean validIcon = mResPackage != null && mIconRes != -1;
+ return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
+ }
+
+ private static String getString(Cursor cursor, String columnName) {
+ return cursor.getString(cursor.getColumnIndex(columnName));
+ }
+
+ private static int getInt(Cursor cursor, String columnName) {
+ return cursor.getInt(cursor.getColumnIndex(columnName));
+ }
+
+ private static int getInt(Cursor cursor, String columnName, int missingValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+ }
+
+ private static long getLong(Cursor cursor, String columnName, long missingValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return cursor.isNull(columnIndex) ? missingValue : cursor.getLong(columnIndex);
+ }
+
+ private static boolean isNull(Cursor cursor, String columnName) {
+ return cursor.isNull(cursor.getColumnIndex(columnName));
+ }
+}
diff --git a/src/com/android/contacts/common/util/DateUtils.java b/src/com/android/contacts/common/util/DateUtils.java
new file mode 100644
index 00000000..f527eb95
--- /dev/null
+++ b/src/com/android/contacts/common/util/DateUtils.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.util;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+
+
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Utility methods for processing dates.
+ */
+public class DateUtils {
+ public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+
+ /**
+ * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year.
+ * Let's add a one-off hack for that day of the year
+ */
+ public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
+
+ // Variations of ISO 8601 date format. Do not change the order - it does affect the
+ // result in ambiguous cases.
+ private static final SimpleDateFormat[] DATE_FORMATS = {
+ CommonDateUtils.FULL_DATE_FORMAT,
+ CommonDateUtils.DATE_AND_TIME_FORMAT,
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
+ };
+
+ static {
+ for (SimpleDateFormat format : DATE_FORMATS) {
+ format.setLenient(true);
+ format.setTimeZone(UTC_TIMEZONE);
+ }
+ CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param string The string representation of the provided date
+ * @param mustContainYear If true, the string is parsed as a date containing a year. If false,
+ * the string is parsed into a valid date even if the year field is missing.
+ * @return A Calendar object corresponding to the date if the string is successfully parsed.
+ * If not, null is returned.
+ */
+ public static Calendar parseDate(String string, boolean mustContainYear) {
+ ParsePosition parsePosition = new ParsePosition(0);
+ Date date;
+ if (!mustContainYear) {
+ final boolean noYearParsed;
+ // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
+ if (NO_YEAR_DATE_FEB29TH.equals(string)) {
+ return getUtcDate(0, Calendar.FEBRUARY, 29);
+ } else {
+ synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
+ date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
+ }
+ noYearParsed = parsePosition.getIndex() == string.length();
+ }
+
+ if (noYearParsed) {
+ return getUtcDate(date, true);
+ }
+ }
+ for (int i = 0; i < DATE_FORMATS.length; i++) {
+ SimpleDateFormat f = DATE_FORMATS[i];
+ synchronized (f) {
+ parsePosition.setIndex(0);
+ date = f.parse(string, parsePosition);
+ if (parsePosition.getIndex() == string.length()) {
+ return getUtcDate(date, false);
+ }
+ }
+ }
+ return null;
+ }
+
+ private static final Calendar getUtcDate(Date date, boolean noYear) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.setTime(date);
+ if (noYear) {
+ calendar.set(Calendar.YEAR, 0);
+ }
+ return calendar;
+ }
+
+ private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.clear();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+ return calendar;
+ }
+
+ public static boolean isYearSet(Calendar cal) {
+ // use the Calendar.YEAR field to track whether or not the year is set instead of
+ // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
+ // true irregardless of what the previous value was
+ return cal.get(Calendar.YEAR) > 1;
+ }
+
+ /**
+ * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with
+ * longForm set to {@code true} by default.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look
+ * like a date, return it unchanged.
+ */
+
+ public static String formatDate(Context context, String string) {
+ return formatDate(context, string, true);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @param longForm If true, return the date formatted into its long string representation.
+ * If false, return the date formatted using its short form representation (i.e. 12/11/2012)
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look
+ * like a date, return it unchanged.
+ */
+ public static String formatDate(Context context, String string, boolean longForm) {
+ if (string == null) {
+ return null;
+ }
+
+ string = string.trim();
+ if (string.length() == 0) {
+ return string;
+ }
+ final Calendar cal = parseDate(string, false);
+
+ // we weren't able to parse the string successfully so just return it unchanged
+ if (cal == null) {
+ return string;
+ }
+
+ final boolean isYearSet = isYearSet(cal);
+ final java.text.DateFormat outFormat;
+ if (!isYearSet) {
+ outFormat = getLocalizedDateFormatWithoutYear(context);
+ } else {
+ outFormat =
+ longForm ? DateFormat.getLongDateFormat(context) :
+ DateFormat.getDateFormat(context);
+ }
+ synchronized (outFormat) {
+ outFormat.setTimeZone(UTC_TIMEZONE);
+ return outFormat.format(cal.getTime());
+ }
+ }
+
+ public static boolean isMonthBeforeDay(Context context) {
+ char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
+ for (int i = 0; i < dateFormatOrder.length; i++) {
+ if (dateFormatOrder[i] == DateFormat.DATE) {
+ return false;
+ }
+ if (dateFormatOrder[i] == DateFormat.MONTH) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a SimpleDateFormat object without the year fields by using a regular expression
+ * to eliminate the year in the string pattern. In the rare occurence that the resulting
+ * pattern cannot be reconverted into a SimpleDateFormat, it uses the provided context to
+ * determine whether the month field should be displayed before the day field, and returns
+ * either "MMMM dd" or "dd MMMM" converted into a SimpleDateFormat.
+ */
+ public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
+ final String pattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(
+ java.text.DateFormat.LONG)).toPattern();
+ // Determine the correct regex pattern for year.
+ // Special case handling for Spanish locale by checking for "de"
+ final String yearPattern = pattern.contains(
+ "de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
+ try {
+ // Eliminate the substring in pattern that matches the format for that of year
+ return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
+ } catch (IllegalArgumentException e) {
+ return new SimpleDateFormat(
+ DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
+ }
+ }
+
+ /**
+ * Given a calendar (possibly containing only a day of the year), returns the earliest possible
+ * anniversary of the date that is equal to or after the current point in time if the date
+ * does not contain a year, or the date converted to the local time zone (if the date contains
+ * a year.
+ *
+ * @param target The date we wish to convert(in the UTC time zone).
+ * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
+ * that is after the current point in time (in the local time zone). Otherwise, returns the
+ * adjusted Date in the local time zone.
+ */
+ public static Date getNextAnnualDate(Calendar target) {
+ final Calendar today = Calendar.getInstance();
+ today.setTime(new Date());
+
+ // Round the current time to the exact start of today so that when we compare
+ // today against the target date, both dates are set to exactly 0000H.
+ today.set(Calendar.HOUR_OF_DAY, 0);
+ today.set(Calendar.MINUTE, 0);
+ today.set(Calendar.SECOND, 0);
+ today.set(Calendar.MILLISECOND, 0);
+
+ final boolean isYearSet = isYearSet(target);
+ final int targetYear = target.get(Calendar.YEAR);
+ final int targetMonth = target.get(Calendar.MONTH);
+ final int targetDay = target.get(Calendar.DAY_OF_MONTH);
+ final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
+ final GregorianCalendar anniversary = new GregorianCalendar();
+ // Convert from the UTC date to the local date. Set the year to today's year if the
+ // there is no provided year (targetYear < 1900)
+ anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear,
+ targetMonth, targetDay);
+ // If the anniversary's date is before the start of today and there is no year set,
+ // increment the year by 1 so that the returned date is always equal to or greater than
+ // today. If the day is a leap year, keep going until we get the next leap year anniversary
+ // Otherwise if there is already a year set, simply return the exact date.
+ if (!isYearSet) {
+ int anniversaryYear = today.get(Calendar.YEAR);
+ if (anniversary.before(today) ||
+ (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
+ // If the target date is not Feb 29, then set the anniversary to the next year.
+ // Otherwise, keep going until we find the next leap year (this is not guaranteed
+ // to be in 4 years time).
+ do {
+ anniversaryYear +=1;
+ } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
+ anniversary.set(anniversaryYear, targetMonth, targetDay);
+ }
+ }
+ return anniversary.getTime();
+ }
+}
diff --git a/src/com/android/contacts/common/util/NameConverter.java b/src/com/android/contacts/common/util/NameConverter.java
new file mode 100644
index 00000000..56f31924
--- /dev/null
+++ b/src/com/android/contacts/common/util/NameConverter.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2011 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.contacts.common.util;
+
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Utility class for converting between a display name and structured name (and vice-versa), via
+ * calls to the contact provider.
+ */
+public class NameConverter {
+
+ /**
+ * The array of fields that comprise a structured name.
+ */
+ public static final String[] STRUCTURED_NAME_FIELDS = new String[] {
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX
+ };
+
+ /**
+ * Converts the given structured name (provided as a map from {@link StructuredName} fields to
+ * corresponding values) into a display name string.
+ * <p>
+ * Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread. See
+ * ContactsProvider2.completeName() for the underlying method call.
+ * @param context Activity context.
+ * @param structuredName The structured name map to convert.
+ * @return The display name computed from the structured name map.
+ */
+ public static String structuredNameToDisplayName(Context context,
+ Map<String, String> structuredName) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (structuredName.containsKey(key)) {
+ appendQueryParameter(builder, key, structuredName.get(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /**
+ * Converts the given structured name (provided as ContentValues) into a display name string.
+ * @param context Activity context.
+ * @param values The content values containing values comprising the structured name.
+ * @return
+ */
+ public static String structuredNameToDisplayName(Context context, ContentValues values) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (values.containsKey(key)) {
+ appendQueryParameter(builder, key, values.getAsString(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /**
+ * Helper method for fetching the display name via the given URI.
+ */
+ private static String fetchDisplayName(Context context, Uri uri) {
+ String displayName = null;
+ Cursor cursor = context.getContentResolver().query(uri, new String[]{
+ StructuredName.DISPLAY_NAME,
+ }, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ displayName = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ return displayName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (as a map from
+ * {@link StructuredName} fields to corresponding values).
+ * <p>
+ * Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @return The structured name map computed from the display name.
+ */
+ public static Map<String, String> displayNameToStructuredName(Context context,
+ String displayName) {
+ Map<String, String> structuredName = new TreeMap<String, String>();
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+
+ appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
+ Cursor cursor = context.getContentResolver().query(builder.build(), STRUCTURED_NAME_FIELDS,
+ null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) {
+ structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return structuredName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (inserting the structured
+ * values into a new or existing ContentValues object).
+ * <p>
+ * Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @param contentValues The content values object to place the structured name values into. If
+ * null, a new one will be created and returned.
+ * @return The ContentValues object containing the structured name fields derived from the
+ * display name.
+ */
+ public static ContentValues displayNameToStructuredName(Context context, String displayName,
+ ContentValues contentValues) {
+ if (contentValues == null) {
+ contentValues = new ContentValues();
+ }
+ Map<String, String> mapValues = displayNameToStructuredName(context, displayName);
+ for (String key : mapValues.keySet()) {
+ contentValues.put(key, mapValues.get(key));
+ }
+ return contentValues;
+ }
+
+ private static void appendQueryParameter(Builder builder, String field, String value) {
+ if (!TextUtils.isEmpty(value)) {
+ builder.appendQueryParameter(field, value);
+ }
+ }
+
+ /**
+ * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues.
+ * Parsed data should be {@link StructuredName#PHONETIC_FAMILY_NAME},
+ * {@link StructuredName#PHONETIC_MIDDLE_NAME}, and
+ * {@link StructuredName#PHONETIC_GIVEN_NAME}.
+ * If this method cannot parse given phoneticName, null values will be stored.
+ *
+ * @param phoneticName Phonetic name to be parsed
+ * @param values ContentValues to be used for storing data. If null, new instance will be
+ * created.
+ * @return ContentValues with parsed data. Those data can be null.
+ */
+ public static StructuredNameDataItem parsePhoneticName(String phoneticName,
+ StructuredNameDataItem item) {
+ String family = null;
+ String middle = null;
+ String given = null;
+
+ if (!TextUtils.isEmpty(phoneticName)) {
+ String[] strings = phoneticName.split(" ", 3);
+ switch (strings.length) {
+ case 1:
+ family = strings[0];
+ break;
+ case 2:
+ family = strings[0];
+ given = strings[1];
+ break;
+ case 3:
+ family = strings[0];
+ middle = strings[1];
+ given = strings[2];
+ break;
+ }
+ }
+
+ if (item == null) {
+ item = new StructuredNameDataItem();
+ }
+ item.setPhoneticFamilyName(family);
+ item.setPhoneticMiddleName(middle);
+ item.setPhoneticGivenName(given);
+ return item;
+ }
+
+ /**
+ * Constructs and returns a phonetic full name from given parts.
+ */
+ public static String buildPhoneticName(String family, String middle, String given) {
+ if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle)
+ || !TextUtils.isEmpty(given)) {
+ StringBuilder sb = new StringBuilder();
+ if (!TextUtils.isEmpty(family)) {
+ sb.append(family.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(middle)) {
+ sb.append(middle.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(given)) {
+ sb.append(given.trim()).append(' ');
+ }
+ sb.setLength(sb.length() - 1); // Yank the last space
+ return sb.toString();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/util/UriUtils.java b/src/com/android/contacts/common/util/UriUtils.java
index 352da489..dbe900b5 100644
--- a/src/com/android/contacts/common/util/UriUtils.java
+++ b/src/com/android/contacts/common/util/UriUtils.java
@@ -50,6 +50,6 @@ public class UriUtils {
}
public static boolean isEncodedContactUri(Uri uri) {
- return uri.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED);
+ return uri != null && uri.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED);
}
}
diff --git a/tests/Android.mk b/tests/Android.mk
index 8ecf594b..d18e2a66 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -5,6 +5,7 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.contacts.common.test
# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/tests/src/com/android/contacts/common/ContactsUtilsTests.java b/tests/src/com/android/contacts/common/ContactsUtilsTests.java
new file mode 100644
index 00000000..c0df3dd4
--- /dev/null
+++ b/tests/src/com/android/contacts/common/ContactsUtilsTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package src.com.android.contacts.common;
+
+import android.content.Intent;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.MoreContactUtils;
+
+/**
+ * Tests for {@link ContactsUtils}.
+ */
+@SmallTest
+public class ContactsUtilsTests extends AndroidTestCase {
+
+ public void testIsGraphicNull() throws Exception {
+ assertFalse(ContactsUtils.isGraphic(null));
+ }
+
+ public void testIsGraphicEmpty() throws Exception {
+ assertFalse(ContactsUtils.isGraphic(""));
+ }
+
+ public void testIsGraphicSpaces() throws Exception {
+ assertFalse(ContactsUtils.isGraphic(" "));
+ }
+
+ public void testIsGraphicPunctuation() throws Exception {
+ assertTrue(ContactsUtils.isGraphic("."));
+ }
+
+ public void testAreObjectsEqual() throws Exception {
+ assertTrue("null:null", ContactsUtils.areObjectsEqual(null, null));
+ assertTrue("1:1", ContactsUtils.areObjectsEqual(1, 1));
+
+ assertFalse("null:1", ContactsUtils.areObjectsEqual(null, 1));
+ assertFalse("1:null", ContactsUtils.areObjectsEqual(1, null));
+ assertFalse("1:2", ContactsUtils.areObjectsEqual(1, 2));
+ }
+
+ public void testAreIntentActionEqual() throws Exception {
+ assertTrue("1", ContactsUtils.areIntentActionEqual(null, null));
+ assertTrue("1", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent("a")));
+
+ assertFalse("11", ContactsUtils.areIntentActionEqual(new Intent("a"), null));
+ assertFalse("12", ContactsUtils.areIntentActionEqual(null, new Intent("a")));
+
+ assertFalse("21", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent()));
+ assertFalse("22", ContactsUtils.areIntentActionEqual(new Intent(), new Intent("b")));
+ assertFalse("23", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent("b")));
+ }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactDeltaListTests.java b/tests/src/com/android/contacts/common/RawContactDeltaListTests.java
new file mode 100644
index 00000000..7f05e694
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactDeltaListTests.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import static android.content.ContentProviderOperation.TYPE_ASSERT;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.RawContactModifierTests.MockContactsSource;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.common.collect.Lists;
+
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Tests for {@link RawContactDeltaList} which focus on "diff" operations that should
+ * create {@link AggregationExceptions} in certain cases.
+ */
+@LargeTest
+public class RawContactDeltaListTests extends AndroidTestCase {
+ public static final String TAG = RawContactDeltaListTests.class.getSimpleName();
+
+ private static final long CONTACT_FIRST = 1;
+ private static final long CONTACT_SECOND = 2;
+
+ public static final long CONTACT_BOB = 10;
+ public static final long CONTACT_MARY = 11;
+
+ public static final long PHONE_RED = 20;
+ public static final long PHONE_GREEN = 21;
+ public static final long PHONE_BLUE = 22;
+
+ public static final long EMAIL_YELLOW = 25;
+
+ public static final long VER_FIRST = 100;
+ public static final long VER_SECOND = 200;
+
+ public static final String TEST_PHONE = "555-1212";
+ public static final String TEST_ACCOUNT = "org.example.test";
+
+ public RawContactDeltaListTests() {
+ super();
+ }
+
+ @Override
+ public void setUp() {
+ mContext = getContext();
+ }
+
+ /**
+ * Build a {@link AccountType} that has various odd constraints for
+ * testing purposes.
+ */
+ protected AccountType getAccountType() {
+ return new MockContactsSource();
+ }
+
+ static ContentValues getValues(ContentProviderOperation operation)
+ throws NoSuchFieldException, IllegalAccessException {
+ final Field field = ContentProviderOperation.class.getDeclaredField("mValues");
+ field.setAccessible(true);
+ return (ContentValues) field.get(operation);
+ }
+
+ static RawContactDelta getUpdate(Context context, long rawContactId) {
+ final RawContact before = RawContactDeltaTests.getRawContact(context, rawContactId,
+ RawContactDeltaTests.TEST_PHONE_ID);
+ return RawContactDelta.fromBefore(before);
+ }
+
+ static RawContactDelta getInsert() {
+ final ContentValues after = new ContentValues();
+ after.put(RawContacts.ACCOUNT_NAME, RawContactDeltaTests.TEST_ACCOUNT_NAME);
+ after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+ return new RawContactDelta(values);
+ }
+
+ static RawContactDeltaList buildSet(RawContactDelta... deltas) {
+ final RawContactDeltaList set = new RawContactDeltaList();
+ Collections.addAll(set, deltas);
+ return set;
+ }
+
+ static RawContactDelta buildBeforeEntity(Context context, long rawContactId, long version,
+ ContentValues... entries) {
+ // Build an existing contact read from database
+ final ContentValues contact = new ContentValues();
+ contact.put(RawContacts.VERSION, version);
+ contact.put(RawContacts._ID, rawContactId);
+ final RawContact before = new RawContact(contact);
+ for (ContentValues entry : entries) {
+ before.addDataItemValues(entry);
+ }
+ return RawContactDelta.fromBefore(before);
+ }
+
+ static RawContactDelta buildAfterEntity(ContentValues... entries) {
+ // Build an existing contact read from database
+ final ContentValues contact = new ContentValues();
+ contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT);
+ final RawContactDelta after = new RawContactDelta(ValuesDelta.fromAfter(contact));
+ for (ContentValues entry : entries) {
+ after.addEntry(ValuesDelta.fromAfter(entry));
+ }
+ return after;
+ }
+
+ static ContentValues buildPhone(long phoneId) {
+ return buildPhone(phoneId, Long.toString(phoneId));
+ }
+
+ static ContentValues buildPhone(long phoneId, String value) {
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, phoneId);
+ values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ values.put(Phone.NUMBER, value);
+ values.put(Phone.TYPE, Phone.TYPE_HOME);
+ return values;
+ }
+
+ static ContentValues buildEmail(long emailId) {
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, emailId);
+ values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ values.put(Email.DATA, Long.toString(emailId));
+ values.put(Email.TYPE, Email.TYPE_HOME);
+ return values;
+ }
+
+ static void insertPhone(RawContactDeltaList set, long rawContactId, ContentValues values) {
+ final RawContactDelta match = set.getByRawContactId(rawContactId);
+ match.addEntry(ValuesDelta.fromAfter(values));
+ }
+
+ static ValuesDelta getPhone(RawContactDeltaList set, long rawContactId, long dataId) {
+ final RawContactDelta match = set.getByRawContactId(rawContactId);
+ return match.getEntry(dataId);
+ }
+
+ static void assertDiffPattern(RawContactDelta delta, ContentProviderOperation... pattern) {
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ delta.buildAssert(diff);
+ delta.buildDiff(diff);
+ assertDiffPattern(diff, pattern);
+ }
+
+ static void assertDiffPattern(RawContactDeltaList set, ContentProviderOperation... pattern) {
+ assertDiffPattern(set.buildDiff(), pattern);
+ }
+
+ static void assertDiffPattern(ArrayList<ContentProviderOperation> diff,
+ ContentProviderOperation... pattern) {
+ assertEquals("Unexpected operations", pattern.length, diff.size());
+ for (int i = 0; i < pattern.length; i++) {
+ final ContentProviderOperation expected = pattern[i];
+ final ContentProviderOperation found = diff.get(i);
+
+ assertEquals("Unexpected uri", expected.getUri(), found.getUri());
+
+ final String expectedType = getStringForType(expected.getType());
+ final String foundType = getStringForType(found.getType());
+ assertEquals("Unexpected type", expectedType, foundType);
+
+ if (expected.getType() == TYPE_DELETE) continue;
+
+ try {
+ final ContentValues expectedValues = getValues(expected);
+ final ContentValues foundValues = getValues(found);
+
+ expectedValues.remove(BaseColumns._ID);
+ foundValues.remove(BaseColumns._ID);
+
+ assertEquals("Unexpected values", expectedValues, foundValues);
+ } catch (NoSuchFieldException e) {
+ fail(e.toString());
+ } catch (IllegalAccessException e) {
+ fail(e.toString());
+ }
+ }
+ }
+
+ static String getStringForType(int type) {
+ switch (type) {
+ case TYPE_ASSERT: return "TYPE_ASSERT";
+ case TYPE_INSERT: return "TYPE_INSERT";
+ case TYPE_UPDATE: return "TYPE_UPDATE";
+ case TYPE_DELETE: return "TYPE_DELETE";
+ default: return Integer.toString(type);
+ }
+ }
+
+ static ContentProviderOperation buildAssertVersion(long version) {
+ final ContentValues values = new ContentValues();
+ values.put(RawContacts.VERSION, version);
+ return buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT, values);
+ }
+
+ static ContentProviderOperation buildAggregationModeUpdate(int mode) {
+ final ContentValues values = new ContentValues();
+ values.put(RawContacts.AGGREGATION_MODE, mode);
+ return buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE, values);
+ }
+
+ static ContentProviderOperation buildUpdateAggregationSuspended() {
+ return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_SUSPENDED);
+ }
+
+ static ContentProviderOperation buildUpdateAggregationDefault() {
+ return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT);
+ }
+
+ static ContentProviderOperation buildUpdateAggregationKeepTogether(long rawContactId) {
+ final ContentValues values = new ContentValues();
+ values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+ values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+ return buildOper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE, values);
+ }
+
+ static ContentValues buildDataInsert(ValuesDelta values, long rawContactId) {
+ final ContentValues insertValues = values.getCompleteValues();
+ insertValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ return insertValues;
+ }
+
+ static ContentProviderOperation buildDelete(Uri uri) {
+ return buildOper(uri, TYPE_DELETE, (ContentValues)null);
+ }
+
+ static ContentProviderOperation buildOper(Uri uri, int type, ValuesDelta values) {
+ return buildOper(uri, type, values.getCompleteValues());
+ }
+
+ static ContentProviderOperation buildOper(Uri uri, int type, ContentValues values) {
+ switch (type) {
+ case TYPE_ASSERT:
+ return ContentProviderOperation.newAssertQuery(uri).withValues(values).build();
+ case TYPE_INSERT:
+ return ContentProviderOperation.newInsert(uri).withValues(values).build();
+ case TYPE_UPDATE:
+ return ContentProviderOperation.newUpdate(uri).withValues(values).build();
+ case TYPE_DELETE:
+ return ContentProviderOperation.newDelete(uri).build();
+ }
+ return null;
+ }
+
+ static Long getVersion(RawContactDeltaList set, Long rawContactId) {
+ return set.getByRawContactId(rawContactId).getValues().getAsLong(RawContacts.VERSION);
+ }
+
+ /**
+ * Count number of {@link AggregationExceptions} updates contained in the
+ * given list of {@link ContentProviderOperation}.
+ */
+ static int countExceptionUpdates(ArrayList<ContentProviderOperation> diff) {
+ int updateCount = 0;
+ for (ContentProviderOperation oper : diff) {
+ if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
+ && oper.getType() == ContentProviderOperation.TYPE_UPDATE) {
+ updateCount++;
+ }
+ }
+ return updateCount;
+ }
+
+ public void testInsert() {
+ final RawContactDelta insert = getInsert();
+ final RawContactDeltaList set = buildSet(insert);
+
+ // Inserting single shouldn't create rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 0, exceptionCount);
+ }
+
+ public void testUpdateUpdate() {
+ final RawContactDelta updateFirst = getUpdate(mContext, CONTACT_FIRST);
+ final RawContactDelta updateSecond = getUpdate(mContext, CONTACT_SECOND);
+ final RawContactDeltaList set = buildSet(updateFirst, updateSecond);
+
+ // Updating two existing shouldn't create rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 0, exceptionCount);
+ }
+
+ public void testUpdateInsert() {
+ final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
+ final RawContactDelta insert = getInsert();
+ final RawContactDeltaList set = buildSet(update, insert);
+
+ // New insert should only create one rule
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 1, exceptionCount);
+ }
+
+ public void testInsertUpdateInsert() {
+ final RawContactDelta insertFirst = getInsert();
+ final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
+ final RawContactDelta insertSecond = getInsert();
+ final RawContactDeltaList set = buildSet(insertFirst, update, insertSecond);
+
+ // Two inserts should create two rules to bind against single existing
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 2, exceptionCount);
+ }
+
+ public void testInsertInsertInsert() {
+ final RawContactDelta insertFirst = getInsert();
+ final RawContactDelta insertSecond = getInsert();
+ final RawContactDelta insertThird = getInsert();
+ final RawContactDeltaList set = buildSet(insertFirst, insertSecond, insertThird);
+
+ // Three new inserts should create only two binding rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 2, exceptionCount);
+ }
+
+ public void testMergeDataRemoteInsert() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+ // Merge in second version, verify they match
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertEquals("Unexpected change when merging", second, merged);
+ }
+
+ public void testMergeDataLocalUpdateRemoteInsert() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+ // Change the local number to trigger update
+ final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+ phone.put(Phone.NUMBER, TEST_PHONE);
+
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+ buildUpdateAggregationDefault());
+
+ // Merge in the second version, verify diff matches
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+ buildUpdateAggregationDefault());
+ }
+
+ public void testMergeDataLocalUpdateRemoteDelete() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_GREEN)));
+
+ // Change the local number to trigger update
+ final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+ phone.put(Phone.NUMBER, TEST_PHONE);
+
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+ buildUpdateAggregationDefault());
+
+ // Merge in the second version, verify that our update changed to
+ // insert, since RED was deleted on remote side
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(phone, CONTACT_BOB)),
+ buildUpdateAggregationDefault());
+ }
+
+ public void testMergeDataLocalDeleteRemoteUpdate() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED, TEST_PHONE)));
+
+ // Delete phone locally
+ final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+ phone.markDeleted();
+
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildDelete(Data.CONTENT_URI),
+ buildUpdateAggregationDefault());
+
+ // Merge in the second version, verify that our delete remains
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildUpdateAggregationSuspended(),
+ buildDelete(Data.CONTENT_URI),
+ buildUpdateAggregationDefault());
+ }
+
+ public void testMergeDataLocalInsertRemoteInsert() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+ // Insert new phone locally
+ final ValuesDelta bluePhone = ValuesDelta.fromAfter(buildPhone(PHONE_BLUE));
+ first.getByRawContactId(CONTACT_BOB).addEntry(bluePhone);
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bluePhone, CONTACT_BOB)),
+ buildUpdateAggregationDefault());
+
+ // Merge in the second version, verify that our insert remains
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bluePhone, CONTACT_BOB)),
+ buildUpdateAggregationDefault());
+ }
+
+ public void testMergeRawContactLocalInsertRemoteInsert() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED)), buildBeforeEntity(mContext, CONTACT_MARY,
+ VER_SECOND, buildPhone(PHONE_RED)));
+
+ // Add new contact locally, should remain insert
+ final ContentValues joePhoneInsert = buildPhone(PHONE_BLUE);
+ final RawContactDelta joeContact = buildAfterEntity(joePhoneInsert);
+ final ContentValues joeContactInsert = joeContact.getValues().getCompleteValues();
+ joeContactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+ first.add(joeContact);
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+ buildUpdateAggregationKeepTogether(CONTACT_BOB));
+
+ // Merge in the second version, verify that our insert remains
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildAssertVersion(VER_SECOND),
+ buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+ buildUpdateAggregationKeepTogether(CONTACT_BOB));
+ }
+
+ public void testMergeRawContactLocalDeleteRemoteDelete() {
+ final RawContactDeltaList first = buildSet(
+ buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+ buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(
+ buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+ // Remove contact locally
+ first.getByRawContactId(CONTACT_MARY).markDeleted();
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildAssertVersion(VER_FIRST),
+ buildDelete(RawContacts.CONTENT_URI));
+
+ // Merge in the second version, verify that our delete isn't needed
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged);
+ }
+
+ public void testMergeRawContactLocalUpdateRemoteDelete() {
+ final RawContactDeltaList first = buildSet(
+ buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+ buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(
+ buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+ // Perform local update
+ final ValuesDelta phone = getPhone(first, CONTACT_MARY, PHONE_RED);
+ phone.put(Phone.NUMBER, TEST_PHONE);
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+ buildUpdateAggregationDefault());
+
+ final ContentValues phoneInsert = phone.getCompleteValues();
+ final ContentValues contactInsert = first.getByRawContactId(CONTACT_MARY).getValues()
+ .getCompleteValues();
+ contactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+
+ // Merge and verify that update turned into insert
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged,
+ buildAssertVersion(VER_SECOND),
+ buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, contactInsert),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, phoneInsert),
+ buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+ buildUpdateAggregationKeepTogether(CONTACT_BOB));
+ }
+
+ public void testMergeUsesNewVersion() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildPhone(PHONE_RED)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildPhone(PHONE_RED)));
+
+ assertEquals((Long)VER_FIRST, getVersion(first, CONTACT_BOB));
+ assertEquals((Long)VER_SECOND, getVersion(second, CONTACT_BOB));
+
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertEquals((Long)VER_SECOND, getVersion(merged, CONTACT_BOB));
+ }
+
+ public void testMergeAfterEnsureAndTrim() {
+ final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_FIRST, buildEmail(EMAIL_YELLOW)));
+ final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+ VER_SECOND, buildEmail(EMAIL_YELLOW)));
+
+ // Ensure we have at least one phone
+ final AccountType source = getAccountType();
+ final RawContactDelta bobContact = first.getByRawContactId(CONTACT_BOB);
+ RawContactModifier.ensureKindExists(bobContact, source, Phone.CONTENT_ITEM_TYPE);
+ final ValuesDelta bobPhone = bobContact.getSuperPrimaryEntry(Phone.CONTENT_ITEM_TYPE, true);
+
+ // Make sure the update would insert a row
+ assertDiffPattern(first,
+ buildAssertVersion(VER_FIRST),
+ buildUpdateAggregationSuspended(),
+ buildOper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bobPhone, CONTACT_BOB)),
+ buildUpdateAggregationDefault());
+
+ // Trim values and ensure that we don't insert things
+ RawContactModifier.trimEmpty(bobContact, source);
+ assertDiffPattern(first);
+
+ // Now re-parent the change, which should remain no-op
+ final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+ assertDiffPattern(merged);
+ }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactDeltaTests.java b/tests/src/com/android/contacts/common/RawContactDeltaTests.java
new file mode 100644
index 00000000..8fc30528
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactDeltaTests.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import static android.content.ContentProviderOperation.TYPE_ASSERT;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.Parcel;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link RawContactDelta} and {@link ValuesDelta}. These tests
+ * focus on passing changes across {@link Parcel}, and verifying that they
+ * correctly build expected "diff" operations.
+ */
+@LargeTest
+public class RawContactDeltaTests extends AndroidTestCase {
+ public static final String TAG = "EntityDeltaTests";
+
+ public static final long TEST_CONTACT_ID = 12;
+ public static final long TEST_PHONE_ID = 24;
+
+ public static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+ public static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+
+ public static final String TEST_ACCOUNT_NAME = "TEST";
+
+ public RawContactDeltaTests() {
+ super();
+ }
+
+ @Override
+ public void setUp() {
+ mContext = getContext();
+ }
+
+ public static RawContact getRawContact(Context context, long contactId, long phoneId) {
+ // Build an existing contact read from database
+ final ContentValues contact = new ContentValues();
+ contact.put(RawContacts.VERSION, 43);
+ contact.put(RawContacts._ID, contactId);
+
+ final ContentValues phone = new ContentValues();
+ phone.put(Data._ID, phoneId);
+ phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+ phone.put(Phone.TYPE, Phone.TYPE_HOME);
+
+ final RawContact before = new RawContact(contact);
+ before.addDataItemValues(phone);
+ return before;
+ }
+
+ /**
+ * Test that {@link RawContactDelta#mergeAfter(RawContactDelta)} correctly passes
+ * any changes through the {@link Parcel} object. This enforces that
+ * {@link RawContactDelta} should be identical when serialized against the same
+ * "before" {@link RawContact}.
+ */
+ public void testParcelChangesNone() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+ final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+ // Merge modified values and assert they match
+ final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+ assertEquals("Unexpected change when merging", source, merged);
+ }
+
+ public void testParcelChangesInsert() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+ final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+ // Add a new row and pass across parcel, should be same
+ final ContentValues phone = new ContentValues();
+ phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+ phone.put(Phone.TYPE, Phone.TYPE_WORK);
+ source.addEntry(ValuesDelta.fromAfter(phone));
+
+ // Merge modified values and assert they match
+ final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+ assertEquals("Unexpected change when merging", source, merged);
+ }
+
+ public void testParcelChangesUpdate() {
+ // Update existing row and pass across parcel, should be same
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+ final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+ final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+ child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+ // Merge modified values and assert they match
+ final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+ assertEquals("Unexpected change when merging", source, merged);
+ }
+
+ public void testParcelChangesDelete() {
+ // Delete a row and pass across parcel, should be same
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+ final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+ final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+ child.markDeleted();
+
+ // Merge modified values and assert they match
+ final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+ assertEquals("Unexpected change when merging", source, merged);
+ }
+
+ public void testValuesDiffDelete() {
+ final ContentValues before = new ContentValues();
+ before.put(Data._ID, TEST_PHONE_ID);
+ before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+ final ValuesDelta values = ValuesDelta.fromBefore(before);
+ values.markDeleted();
+
+ // Should produce a delete action
+ final Builder builder = values.buildDiff(Data.CONTENT_URI);
+ final int type = builder.build().getType();
+ assertEquals("Didn't produce delete action", TYPE_DELETE, type);
+ }
+
+ /**
+ * Test that {@link RawContactDelta#buildDiff(ArrayList)} is correctly built for
+ * insert, update, and delete cases. This only tests a subset of possible
+ * {@link Data} row changes.
+ */
+ public void testEntityDiffNone() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+ // Assert that writing unchanged produces few operations
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildDiff(diff);
+
+ assertTrue("Created changes when none needed", (diff.size() == 0));
+ }
+
+ public void testEntityDiffNoneInsert() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+ // Insert a new phone number
+ final ContentValues phone = new ContentValues();
+ phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+ phone.put(Phone.TYPE, Phone.TYPE_WORK);
+ source.addEntry(ValuesDelta.fromAfter(phone));
+
+ // Assert two operations: insert Data row and enforce version
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 4, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected version enforcement", TYPE_ASSERT, oper.getType());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(3);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testEntityDiffUpdateInsert() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+ // Update parent contact values
+ source.getValues().put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
+
+ // Insert a new phone number
+ final ContentValues phone = new ContentValues();
+ phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+ phone.put(Phone.TYPE, Phone.TYPE_WORK);
+ source.addEntry(ValuesDelta.fromAfter(phone));
+
+ // Assert three operations: update Contact, insert Data row, enforce version
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 5, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected version enforcement", TYPE_ASSERT, oper.getType());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(3);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(4);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testEntityDiffNoneUpdate() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+ // Update existing phone number
+ final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+ child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+ // Assert that version is enforced
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 4, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected version enforcement", TYPE_ASSERT, oper.getType());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(3);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testEntityDiffDelete() {
+ final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+ final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+ // Delete entire entity
+ source.getValues().markDeleted();
+
+ // Assert two operations: delete Contact and enforce version
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 2, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected version enforcement", TYPE_ASSERT, oper.getType());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testEntityDiffInsert() {
+ // Insert a RawContact
+ final ContentValues after = new ContentValues();
+ after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+ after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+ final RawContactDelta source = new RawContactDelta(values);
+
+ // Assert two operations: delete Contact and enforce version
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 2, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testEntityDiffInsertInsert() {
+ // Insert a RawContact
+ final ContentValues after = new ContentValues();
+ after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+ after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+ final RawContactDelta source = new RawContactDelta(values);
+
+ // Insert a new phone number
+ final ContentValues phone = new ContentValues();
+ phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+ phone.put(Phone.TYPE, Phone.TYPE_WORK);
+ source.addEntry(ValuesDelta.fromAfter(phone));
+
+ // Assert two operations: delete Contact and enforce version
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactModifierTests.java b/tests/src/com/android/contacts/common/RawContactModifierTests.java
new file mode 100644
index 00000000..2e972cfb
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactModifierTests.java
@@ -0,0 +1,1235 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.test.mocks.ContactsMockContext;
+import com.android.contacts.common.test.mocks.MockAccountTypeManager;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link RawContactModifier} to verify that {@link AccountType}
+ * constraints are being enforced correctly.
+ */
+@LargeTest
+public class RawContactModifierTests extends AndroidTestCase {
+ public static final String TAG = "EntityModifierTests";
+
+ public static final long VER_FIRST = 100;
+
+ private static final long TEST_ID = 4;
+ private static final String TEST_PHONE = "218-555-1212";
+ private static final String TEST_NAME = "Adam Young";
+ private static final String TEST_NAME2 = "Breanne Duren";
+ private static final String TEST_IM = "example@example.com";
+ private static final String TEST_POSTAL = "1600 Amphitheatre Parkway";
+
+ private static final String TEST_ACCOUNT_NAME = "unittest@example.com";
+ private static final String TEST_ACCOUNT_TYPE = "com.example.unittest";
+
+ private static final String EXCHANGE_ACCT_TYPE = "com.android.exchange";
+
+ @Override
+ public void setUp() {
+ mContext = getContext();
+ }
+
+ public static class MockContactsSource extends AccountType {
+
+ MockContactsSource() {
+ try {
+ this.accountType = TEST_ACCOUNT_TYPE;
+
+ final DataKind nameKind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+ R.string.nameLabelsGroup, -1, true);
+ nameKind.typeOverallMax = 1;
+ addKind(nameKind);
+
+ // Phone allows maximum 2 home, 1 work, and unlimited other, with
+ // constraint of 5 numbers maximum.
+ final DataKind phoneKind = new DataKind(
+ Phone.CONTENT_ITEM_TYPE, -1, 10, true);
+
+ phoneKind.typeOverallMax = 5;
+ phoneKind.typeColumn = Phone.TYPE;
+ phoneKind.typeList = Lists.newArrayList();
+ phoneKind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
+ phoneKind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
+ phoneKind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
+ phoneKind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
+
+ phoneKind.fieldList = Lists.newArrayList();
+ phoneKind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
+ phoneKind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
+
+ addKind(phoneKind);
+
+ // Email is unlimited
+ final DataKind emailKind = new DataKind(Email.CONTENT_ITEM_TYPE, -1, 10, true);
+ emailKind.typeOverallMax = -1;
+ emailKind.fieldList = Lists.newArrayList();
+ emailKind.fieldList.add(new EditField(Email.DATA, -1, -1));
+ addKind(emailKind);
+
+ // IM is only one
+ final DataKind imKind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, 10, true);
+ imKind.typeOverallMax = 1;
+ imKind.fieldList = Lists.newArrayList();
+ imKind.fieldList.add(new EditField(Im.DATA, -1, -1));
+ addKind(imKind);
+
+ // Organization is only one
+ final DataKind orgKind = new DataKind(Organization.CONTENT_ITEM_TYPE, -1, 10, true);
+ orgKind.typeOverallMax = 1;
+ orgKind.fieldList = Lists.newArrayList();
+ orgKind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
+ orgKind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
+ addKind(orgKind);
+ } catch (DefinitionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return false;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+ }
+
+ /**
+ * Build a {@link AccountType} that has various odd constraints for
+ * testing purposes.
+ */
+ protected AccountType getAccountType() {
+ return new MockContactsSource();
+ }
+
+ /**
+ * Build {@link AccountTypeManager} instance.
+ */
+ protected AccountTypeManager getAccountTypes(AccountType... types) {
+ return new MockAccountTypeManager(types, null);
+ }
+
+ /**
+ * Build an {@link RawContact} with the requested set of phone numbers.
+ */
+ protected RawContactDelta getRawContact(Long existingId, ContentValues... entries) {
+ final ContentValues contact = new ContentValues();
+ if (existingId != null) {
+ contact.put(RawContacts._ID, existingId);
+ }
+ contact.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+ contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE);
+
+ final RawContact before = new RawContact(contact);
+ for (ContentValues values : entries) {
+ before.addDataItemValues(values);
+ }
+ return RawContactDelta.fromBefore(before);
+ }
+
+ /**
+ * Assert this {@link List} contains the given {@link Object}.
+ */
+ protected void assertContains(List<?> list, Object object) {
+ assertTrue("Missing expected value", list.contains(object));
+ }
+
+ /**
+ * Assert this {@link List} does not contain the given {@link Object}.
+ */
+ protected void assertNotContains(List<?> list, Object object) {
+ assertFalse("Contained unexpected value", list.contains(object));
+ }
+
+ /**
+ * Insert various rows to test
+ * {@link RawContactModifier#getValidTypes(RawContactDelta, DataKind, EditType)}
+ */
+ public void testValidTypes() {
+ // Build a source and pull specific types
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+ final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+ final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+ List<EditType> validTypes;
+
+ // Add first home, first work
+ final RawContactDelta state = getRawContact(TEST_ID);
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ RawContactModifier.insertChild(state, kindPhone, typeWork);
+
+ // Expecting home, other
+ validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
+ assertContains(validTypes, typeHome);
+ assertNotContains(validTypes, typeWork);
+ assertContains(validTypes, typeOther);
+
+ // Add second home
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+ // Expecting other
+ validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
+ assertNotContains(validTypes, typeHome);
+ assertNotContains(validTypes, typeWork);
+ assertContains(validTypes, typeOther);
+
+ // Add third and fourth home (invalid, but possible)
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+ // Expecting none
+ validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
+ assertNotContains(validTypes, typeHome);
+ assertNotContains(validTypes, typeWork);
+ assertNotContains(validTypes, typeOther);
+ }
+
+ /**
+ * Test {@link RawContactModifier#canInsert(RawContactDelta, DataKind)} by
+ * inserting various rows.
+ */
+ public void testCanInsert() {
+ // Build a source and pull specific types
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+ final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+ final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+ // Add first home, first work
+ final RawContactDelta state = getRawContact(TEST_ID);
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ RawContactModifier.insertChild(state, kindPhone, typeWork);
+ assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));
+
+ // Add two other, which puts us just under "5" overall limit
+ RawContactModifier.insertChild(state, kindPhone, typeOther);
+ RawContactModifier.insertChild(state, kindPhone, typeOther);
+ assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));
+
+ // Add second home, which should push to snug limit
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ assertFalse("Able to insert", RawContactModifier.canInsert(state, kindPhone));
+ }
+
+ /**
+ * Test
+ * {@link RawContactModifier#getBestValidType(RawContactDelta, DataKind, boolean, int)}
+ * by asserting expected best options in various states.
+ */
+ public void testBestValidType() {
+ // Build a source and pull specific types
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+ final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+ final EditType typeFaxWork = RawContactModifier.getType(kindPhone, Phone.TYPE_FAX_WORK);
+ final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+ EditType suggested;
+
+ // Default suggestion should be home
+ final RawContactDelta state = getRawContact(TEST_ID);
+ suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+ assertEquals("Unexpected suggestion", typeHome, suggested);
+
+ // Add first home, should now suggest work
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+ assertEquals("Unexpected suggestion", typeWork, suggested);
+
+ // Add work fax, should still suggest work
+ RawContactModifier.insertChild(state, kindPhone, typeFaxWork);
+ suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+ assertEquals("Unexpected suggestion", typeWork, suggested);
+
+ // Add other, should still suggest work
+ RawContactModifier.insertChild(state, kindPhone, typeOther);
+ suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+ assertEquals("Unexpected suggestion", typeWork, suggested);
+
+ // Add work, now should suggest other
+ RawContactModifier.insertChild(state, kindPhone, typeWork);
+ suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+ assertEquals("Unexpected suggestion", typeOther, suggested);
+ }
+
+ public void testIsEmptyEmpty() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+ // Test entirely empty row
+ final ContentValues after = new ContentValues();
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+
+ assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));
+ }
+
+ public void testIsEmptyDirectFields() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Test row that has type values, but core fields are empty
+ final RawContactDelta state = getRawContact(TEST_ID);
+ final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+ assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));
+
+ // Insert some data to trigger non-empty state
+ values.put(Phone.NUMBER, TEST_PHONE);
+
+ assertFalse("Expected non-empty", RawContactModifier.isEmpty(values, kindPhone));
+ }
+
+ public void testTrimEmptySingle() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Test row that has type values, but core fields are empty
+ final RawContactDelta state = getRawContact(TEST_ID);
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+ // Build diff, expecting insert for data row and update enforcement
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Trim empty rows and try again, expecting delete of overall contact
+ RawContactModifier.trimEmpty(state, source);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testTrimEmptySpaces() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Test row that has type values, but values are spaces
+ final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
+ VER_FIRST);
+ final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+ values.put(Phone.NUMBER, " ");
+
+ // Build diff, expecting insert for data row and update enforcement
+ RawContactDeltaListTests.assertDiffPattern(state,
+ RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+ RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+ RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
+ RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+ RawContactDeltaListTests.buildUpdateAggregationDefault());
+
+ // Trim empty rows and try again, expecting delete of overall contact
+ RawContactModifier.trimEmpty(state, source);
+ RawContactDeltaListTests.assertDiffPattern(state,
+ RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+ RawContactDeltaListTests.buildDelete(RawContacts.CONTENT_URI));
+ }
+
+ public void testTrimLeaveValid() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Test row that has type values with valid number
+ final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
+ VER_FIRST);
+ final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+ values.put(Phone.NUMBER, TEST_PHONE);
+
+ // Build diff, expecting insert for data row and update enforcement
+ RawContactDeltaListTests.assertDiffPattern(state,
+ RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+ RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+ RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
+ RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+ RawContactDeltaListTests.buildUpdateAggregationDefault());
+
+ // Trim empty rows and try again, expecting no differences
+ RawContactModifier.trimEmpty(state, source);
+ RawContactDeltaListTests.assertDiffPattern(state,
+ RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+ RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+ RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
+ RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+ RawContactDeltaListTests.buildUpdateAggregationDefault());
+ }
+
+ public void testTrimEmptyUntouched() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" that has empty row
+ final RawContactDelta state = getRawContact(TEST_ID);
+ final ContentValues before = new ContentValues();
+ before.put(Data._ID, TEST_ID);
+ before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ state.addEntry(ValuesDelta.fromBefore(before));
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Try trimming existing empty, which we shouldn't touch
+ RawContactModifier.trimEmpty(state, source);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimEmptyAfterUpdate() {
+ final AccountType source = getAccountType();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" that has row with some phone number
+ final ContentValues before = new ContentValues();
+ before.put(Data._ID, TEST_ID);
+ before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ before.put(kindPhone.typeColumn, typeHome.rawValue);
+ before.put(Phone.NUMBER, TEST_PHONE);
+ final RawContactDelta state = getRawContact(TEST_ID, before);
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Now update row by changing number to empty string, expecting single update
+ final ValuesDelta child = state.getEntry(TEST_ID);
+ child.put(Phone.NUMBER, "");
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Now run trim, which should turn that update into delete
+ RawContactModifier.trimEmpty(state, source);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testTrimInsertEmpty() {
+ final AccountType accountType = getAccountType();
+ final AccountTypeManager accountTypes = getAccountTypes(accountType);
+ final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Try creating a contact without any child entries
+ final RawContactDelta state = getRawContact(null);
+ final RawContactDeltaList set = new RawContactDeltaList();
+ set.add(state);
+
+
+ // Build diff, expecting single insert
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 2, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Trim empty rows and try again, expecting no insert
+ RawContactModifier.trimEmpty(set, accountTypes);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimInsertInsert() {
+ final AccountType accountType = getAccountType();
+ final AccountTypeManager accountTypes = getAccountTypes(accountType);
+ final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Try creating a contact with single empty entry
+ final RawContactDelta state = getRawContact(null);
+ RawContactModifier.insertChild(state, kindPhone, typeHome);
+ final RawContactDeltaList set = new RawContactDeltaList();
+ set.add(state);
+
+ // Build diff, expecting two insert operations
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+
+ // Trim empty rows and try again, expecting silence
+ RawContactModifier.trimEmpty(set, accountTypes);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimUpdateRemain() {
+ final AccountType accountType = getAccountType();
+ final AccountTypeManager accountTypes = getAccountTypes(accountType);
+ final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" with two phone numbers
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ first.put(kindPhone.typeColumn, typeHome.rawValue);
+ first.put(Phone.NUMBER, TEST_PHONE);
+
+ final ContentValues second = new ContentValues();
+ second.put(Data._ID, TEST_ID);
+ second.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ second.put(kindPhone.typeColumn, typeHome.rawValue);
+ second.put(Phone.NUMBER, TEST_PHONE);
+
+ final RawContactDelta state = getRawContact(TEST_ID, first, second);
+ final RawContactDeltaList set = new RawContactDeltaList();
+ set.add(state);
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Now update row by changing number to empty string, expecting single update
+ final ValuesDelta child = state.getEntry(TEST_ID);
+ child.put(Phone.NUMBER, "");
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Now run trim, which should turn that update into delete
+ RawContactModifier.trimEmpty(set, accountTypes);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testTrimUpdateUpdate() {
+ final AccountType accountType = getAccountType();
+ final AccountTypeManager accountTypes = getAccountTypes(accountType);
+ final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" with two phone numbers
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ first.put(kindPhone.typeColumn, typeHome.rawValue);
+ first.put(Phone.NUMBER, TEST_PHONE);
+
+ final RawContactDelta state = getRawContact(TEST_ID, first);
+ final RawContactDeltaList set = new RawContactDeltaList();
+ set.add(state);
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Now update row by changing number to empty string, expecting single update
+ final ValuesDelta child = state.getEntry(TEST_ID);
+ child.put(Phone.NUMBER, "");
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Now run trim, which should turn into deleting the whole contact
+ RawContactModifier.trimEmpty(set, accountTypes);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testParseExtrasExistingName() {
+ final AccountType accountType = getAccountType();
+
+ // Build "before" name
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ first.put(StructuredName.GIVEN_NAME, TEST_NAME);
+
+ // Parse extras, making sure we keep single name
+ final RawContactDelta state = getRawContact(TEST_ID, first);
+ final Bundle extras = new Bundle();
+ extras.putString(Insert.NAME, TEST_NAME2);
+ RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+ final int nameCount = state.getMimeEntriesCount(StructuredName.CONTENT_ITEM_TYPE, true);
+ assertEquals("Unexpected names", 1, nameCount);
+ }
+
+ public void testParseExtrasIgnoreLimit() {
+ final AccountType accountType = getAccountType();
+
+ // Build "before" IM
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ first.put(Im.DATA, TEST_IM);
+
+ final RawContactDelta state = getRawContact(TEST_ID, first);
+ final int beforeCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
+
+ // We should ignore data that doesn't fit account type rules, since account type
+ // only allows single Im
+ final Bundle extras = new Bundle();
+ extras.putInt(Insert.IM_PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+ extras.putString(Insert.IM_HANDLE, TEST_IM);
+ RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+ final int afterCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
+ assertEquals("Broke account type rules", beforeCount, afterCount);
+ }
+
+ public void testParseExtrasIgnoreUnhandled() {
+ final AccountType accountType = getAccountType();
+ final RawContactDelta state = getRawContact(TEST_ID);
+
+ // We should silently ignore types unsupported by account type
+ final Bundle extras = new Bundle();
+ extras.putString(Insert.POSTAL, TEST_POSTAL);
+ RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+ assertNull("Broke accoun type rules",
+ state.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
+ }
+
+ public void testParseExtrasJobTitle() {
+ final AccountType accountType = getAccountType();
+ final RawContactDelta state = getRawContact(TEST_ID);
+
+ // Make sure that we create partial Organizations
+ final Bundle extras = new Bundle();
+ extras.putString(Insert.JOB_TITLE, TEST_NAME);
+ RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+ final int count = state.getMimeEntries(Organization.CONTENT_ITEM_TYPE).size();
+ assertEquals("Expected to create organization", 1, count);
+ }
+
+ public void testMigrateWithDisplayNameFromGoogleToExchange1() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+ ContactsMockContext context = new ContactsMockContext(getContext());
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ mockNameValues.put(StructuredName.PREFIX, "prefix");
+ mockNameValues.put(StructuredName.GIVEN_NAME, "given");
+ mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
+ mockNameValues.put(StructuredName.FAMILY_NAME, "family");
+ mockNameValues.put(StructuredName.SUFFIX, "suffix");
+ mockNameValues.put(StructuredName.PHONETIC_FAMILY_NAME, "PHONETIC_FAMILY");
+ mockNameValues.put(StructuredName.PHONETIC_MIDDLE_NAME, "PHONETIC_MIDDLE");
+ mockNameValues.put(StructuredName.PHONETIC_GIVEN_NAME, "PHONETIC_GIVEN");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+ List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+ assertEquals(1, list.size());
+
+ ContentValues output = list.get(0).getAfter();
+ assertEquals("prefix", output.getAsString(StructuredName.PREFIX));
+ assertEquals("given", output.getAsString(StructuredName.GIVEN_NAME));
+ assertEquals("middle", output.getAsString(StructuredName.MIDDLE_NAME));
+ assertEquals("family", output.getAsString(StructuredName.FAMILY_NAME));
+ assertEquals("suffix", output.getAsString(StructuredName.SUFFIX));
+ // Phonetic middle name isn't supported by Exchange.
+ assertEquals("PHONETIC_FAMILY", output.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
+ assertEquals("PHONETIC_GIVEN", output.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+ }
+
+ public void testMigrateWithDisplayNameFromGoogleToExchange2() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+ ContactsMockContext context = new ContactsMockContext(getContext());
+ MockContentProvider provider = context.getContactsProvider();
+
+ String inputDisplayName = "prefix given middle family suffix";
+ // The method will ask the provider to split/join StructuredName.
+ Uri uriForBuildDisplayName =
+ ContactsContract.AUTHORITY_URI
+ .buildUpon()
+ .appendPath("complete_name")
+ .appendQueryParameter(StructuredName.DISPLAY_NAME, inputDisplayName)
+ .build();
+ provider.expectQuery(uriForBuildDisplayName)
+ .returnRow("prefix", "given", "middle", "family", "suffix")
+ .withProjection(StructuredName.PREFIX, StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ mockNameValues.put(StructuredName.DISPLAY_NAME, inputDisplayName);
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+ List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+ assertEquals(1, list.size());
+
+ ContentValues outputValues = list.get(0).getAfter();
+ assertEquals("prefix", outputValues.getAsString(StructuredName.PREFIX));
+ assertEquals("given", outputValues.getAsString(StructuredName.GIVEN_NAME));
+ assertEquals("middle", outputValues.getAsString(StructuredName.MIDDLE_NAME));
+ assertEquals("family", outputValues.getAsString(StructuredName.FAMILY_NAME));
+ assertEquals("suffix", outputValues.getAsString(StructuredName.SUFFIX));
+ }
+
+ public void testMigrateWithStructuredNameFromExchangeToGoogle() {
+ AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ AccountType newAccountType = new GoogleAccountType(getContext(), "");
+ DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+ ContactsMockContext context = new ContactsMockContext(getContext());
+ MockContentProvider provider = context.getContactsProvider();
+
+ // The method will ask the provider to split/join StructuredName.
+ Uri uriForBuildDisplayName =
+ ContactsContract.AUTHORITY_URI
+ .buildUpon()
+ .appendPath("complete_name")
+ .appendQueryParameter(StructuredName.PREFIX, "prefix")
+ .appendQueryParameter(StructuredName.GIVEN_NAME, "given")
+ .appendQueryParameter(StructuredName.MIDDLE_NAME, "middle")
+ .appendQueryParameter(StructuredName.FAMILY_NAME, "family")
+ .appendQueryParameter(StructuredName.SUFFIX, "suffix")
+ .build();
+ provider.expectQuery(uriForBuildDisplayName)
+ .returnRow("prefix given middle family suffix")
+ .withProjection(StructuredName.DISPLAY_NAME);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ mockNameValues.put(StructuredName.PREFIX, "prefix");
+ mockNameValues.put(StructuredName.GIVEN_NAME, "given");
+ mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
+ mockNameValues.put(StructuredName.FAMILY_NAME, "family");
+ mockNameValues.put(StructuredName.SUFFIX, "suffix");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size());
+ ContentValues outputValues = list.get(0).getAfter();
+ assertEquals("prefix given middle family suffix",
+ outputValues.getAsString(StructuredName.DISPLAY_NAME));
+ }
+
+ public void testMigratePostalFromGoogleToExchange() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+ mockNameValues.put(StructuredPostal.FORMATTED_ADDRESS, "formatted_address");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migratePostal(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size());
+ ContentValues outputValues = list.get(0).getAfter();
+ // FORMATTED_ADDRESS isn't supported by Exchange.
+ assertNull(outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS));
+ assertEquals("formatted_address", outputValues.getAsString(StructuredPostal.STREET));
+ }
+
+ public void testMigratePostalFromExchangeToGoogle() {
+ AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ AccountType newAccountType = new GoogleAccountType(getContext(), "");
+ DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+ mockNameValues.put(StructuredPostal.COUNTRY, "country");
+ mockNameValues.put(StructuredPostal.POSTCODE, "postcode");
+ mockNameValues.put(StructuredPostal.REGION, "region");
+ mockNameValues.put(StructuredPostal.CITY, "city");
+ mockNameValues.put(StructuredPostal.STREET, "street");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migratePostal(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size());
+ ContentValues outputValues = list.get(0).getAfter();
+
+ // Check FORMATTED_ADDRESS contains all info.
+ String formattedAddress = outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ assertNotNull(formattedAddress);
+ assertTrue(formattedAddress.contains("country"));
+ assertTrue(formattedAddress.contains("postcode"));
+ assertTrue(formattedAddress.contains("region"));
+ assertTrue(formattedAddress.contains("postcode"));
+ assertTrue(formattedAddress.contains("city"));
+ assertTrue(formattedAddress.contains("street"));
+ }
+
+ public void testMigrateEventFromGoogleToExchange1() {
+ testMigrateEventCommon(new GoogleAccountType(getContext(), ""),
+ new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE));
+ }
+
+ public void testMigrateEventFromExchangeToGoogle() {
+ testMigrateEventCommon(new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE),
+ new GoogleAccountType(getContext(), ""));
+ }
+
+ private void testMigrateEventCommon(AccountType oldAccountType, AccountType newAccountType) {
+ DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Event.START_DATE, "1972-02-08");
+ mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateEvent(oldState, newState, kind, 1990);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size()); // Anniversary should be dropped.
+ ContentValues outputValues = list.get(0).getAfter();
+
+ assertEquals("1972-02-08", outputValues.getAsString(Event.START_DATE));
+ assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
+ }
+
+ public void testMigrateEventFromGoogleToExchange2() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+ // No year format is not supported by Exchange.
+ mockNameValues.put(Event.START_DATE, "--06-01");
+ mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Event.START_DATE, "1980-08-02");
+ // Anniversary is not supported by Exchange
+ mockNameValues.put(Event.TYPE, Event.TYPE_ANNIVERSARY);
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateEvent(oldState, newState, kind, 1990);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size()); // Anniversary should be dropped.
+ ContentValues outputValues = list.get(0).getAfter();
+
+ // Default year should be used.
+ assertEquals("1990-06-01", outputValues.getAsString(Event.START_DATE));
+ assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
+ }
+
+ public void testMigrateEmailFromGoogleToExchange() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Email.TYPE, Email.TYPE_CUSTOM);
+ mockNameValues.put(Email.LABEL, "custom_type");
+ mockNameValues.put(Email.ADDRESS, "address1");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Email.TYPE, Email.TYPE_HOME);
+ mockNameValues.put(Email.ADDRESS, "address2");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Email.TYPE, Email.TYPE_WORK);
+ mockNameValues.put(Email.ADDRESS, "address3");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ // Exchange can have up to 3 email entries. This 4th entry should be dropped.
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Email.TYPE, Email.TYPE_OTHER);
+ mockNameValues.put(Email.ADDRESS, "address4");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Email.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(3, list.size());
+
+ ContentValues outputValues = list.get(0).getAfter();
+ assertEquals(Email.TYPE_CUSTOM, outputValues.getAsInteger(Email.TYPE).intValue());
+ assertEquals("custom_type", outputValues.getAsString(Email.LABEL));
+ assertEquals("address1", outputValues.getAsString(Email.ADDRESS));
+
+ outputValues = list.get(1).getAfter();
+ assertEquals(Email.TYPE_HOME, outputValues.getAsInteger(Email.TYPE).intValue());
+ assertEquals("address2", outputValues.getAsString(Email.ADDRESS));
+
+ outputValues = list.get(2).getAfter();
+ assertEquals(Email.TYPE_WORK, outputValues.getAsInteger(Email.TYPE).intValue());
+ assertEquals("address3", outputValues.getAsString(Email.ADDRESS));
+ }
+
+ public void testMigrateImFromGoogleToExchange() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ // Exchange doesn't support TYPE_HOME
+ mockNameValues.put(Im.TYPE, Im.TYPE_HOME);
+ mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_JABBER);
+ mockNameValues.put(Im.DATA, "im1");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ // Exchange doesn't support TYPE_WORK
+ mockNameValues.put(Im.TYPE, Im.TYPE_WORK);
+ mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_YAHOO);
+ mockNameValues.put(Im.DATA, "im2");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
+ mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+ mockNameValues.put(Im.CUSTOM_PROTOCOL, "custom_protocol");
+ mockNameValues.put(Im.DATA, "im3");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ // Exchange can have up to 3 IM entries. This 4th entry should be dropped.
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
+ mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+ mockNameValues.put(Im.DATA, "im4");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Im.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(3, list.size());
+
+ assertNotNull(kind.defaultValues.getAsInteger(Im.TYPE));
+
+ int defaultType = kind.defaultValues.getAsInteger(Im.TYPE);
+
+ ContentValues outputValues = list.get(0).getAfter();
+ // HOME should become default type.
+ assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+ assertEquals(Im.PROTOCOL_JABBER, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+ assertEquals("im1", outputValues.getAsString(Im.DATA));
+
+ outputValues = list.get(1).getAfter();
+ assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+ assertEquals(Im.PROTOCOL_YAHOO, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+ assertEquals("im2", outputValues.getAsString(Im.DATA));
+
+ outputValues = list.get(2).getAfter();
+ assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+ assertEquals(Im.PROTOCOL_CUSTOM, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+ assertEquals("custom_protocol", outputValues.getAsString(Im.CUSTOM_PROTOCOL));
+ assertEquals("im3", outputValues.getAsString(Im.DATA));
+ }
+
+ public void testMigratePhoneFromGoogleToExchange() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+ // Create 5 numbers.
+ // - "1" -- HOME
+ // - "2" -- WORK
+ // - "3" -- CUSTOM
+ // - "4" -- WORK
+ // - "5" -- WORK_MOBILE
+ // Then we convert it to Exchange account type.
+ // - "1" -- HOME
+ // - "2" -- WORK
+ // - "3" -- Because CUSTOM is not supported, it'll be changed to the default, MOBILE
+ // - "4" -- WORK
+ // - "5" -- WORK_MOBILE not suppoted again, so will be MOBILE.
+ // But then, Exchange doesn't support multiple MOBILE numbers, so "5" will be removed.
+ // i.e. the result will be:
+ // - "1" -- HOME
+ // - "2" -- WORK
+ // - "3" -- MOBILE
+ // - "4" -- WORK
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Phone.TYPE, Phone.TYPE_HOME);
+ mockNameValues.put(Phone.NUMBER, "1");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
+ mockNameValues.put(Phone.NUMBER, "2");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ // Exchange doesn't support this type. Default to MOBILE
+ mockNameValues.put(Phone.TYPE, Phone.TYPE_CUSTOM);
+ mockNameValues.put(Phone.LABEL, "custom_type");
+ mockNameValues.put(Phone.NUMBER, "3");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
+ mockNameValues.put(Phone.NUMBER, "4");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+ mockNameValues = new ContentValues();
+
+ mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK_MOBILE);
+ mockNameValues.put(Phone.NUMBER, "5");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Phone.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(4, list.size());
+
+ int defaultType = Phone.TYPE_MOBILE;
+
+ ContentValues outputValues = list.get(0).getAfter();
+ assertEquals(Phone.TYPE_HOME, outputValues.getAsInteger(Phone.TYPE).intValue());
+ assertEquals("1", outputValues.getAsString(Phone.NUMBER));
+ outputValues = list.get(1).getAfter();
+ assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
+ assertEquals("2", outputValues.getAsString(Phone.NUMBER));
+ outputValues = list.get(2).getAfter();
+ assertEquals(defaultType, outputValues.getAsInteger(Phone.TYPE).intValue());
+ assertNull(outputValues.getAsInteger(Phone.LABEL));
+ assertEquals("3", outputValues.getAsString(Phone.NUMBER));
+ outputValues = list.get(3).getAfter();
+ assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
+ assertEquals("4", outputValues.getAsString(Phone.NUMBER));
+ }
+
+ public void testMigrateOrganizationFromGoogleToExchange() {
+ AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+ AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+ DataKind kind = newAccountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+
+ RawContactDelta oldState = new RawContactDelta();
+ ContentValues mockNameValues = new ContentValues();
+ mockNameValues.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+ mockNameValues.put(Organization.COMPANY, "company1");
+ mockNameValues.put(Organization.DEPARTMENT, "department1");
+ oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+ RawContactDelta newState = new RawContactDelta();
+ RawContactModifier.migrateGenericWithoutTypeColumn(oldState, newState, kind);
+
+ List<ValuesDelta> list = newState.getMimeEntries(Organization.CONTENT_ITEM_TYPE);
+ assertNotNull(list);
+ assertEquals(1, list.size());
+
+ ContentValues outputValues = list.get(0).getAfter();
+ assertEquals("company1", outputValues.getAsString(Organization.COMPANY));
+ assertEquals("department1", outputValues.getAsString(Organization.DEPARTMENT));
+ }
+}
diff --git a/tests/src/com/android/contacts/common/model/ContactLoaderTest.java b/tests/src/com/android/contacts/common/model/ContactLoaderTest.java
new file mode 100644
index 00000000..94c64ebb
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/ContactLoaderTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2010 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.contacts.common.model;
+
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.LoaderTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.test.mocks.ContactsMockContext;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.BaseAccountType;
+import com.android.contacts.common.test.InjectedServices;
+import com.android.contacts.common.test.mocks.MockAccountTypeManager;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail and editor view.
+ */
+@LargeTest
+public class ContactLoaderTest extends LoaderTestCase {
+ private ContactsMockContext mMockContext;
+ private MockContentProvider mContactsProvider;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMockContext = new ContactsMockContext(getContext());
+ mContactsProvider = mMockContext.getContactsProvider();
+
+ InjectedServices services = new InjectedServices();
+ AccountType accountType = new BaseAccountType() {
+ @Override
+ public boolean areContactsWritable() {
+ return false;
+ }
+ };
+ accountType.accountType = "mockAccountType";
+
+ AccountWithDataSet account =
+ new AccountWithDataSet("mockAccountName", "mockAccountType", null);
+
+ AccountTypeManager.setInstanceForTest(
+ new MockAccountTypeManager(
+ new AccountType[]{accountType}, new AccountWithDataSet[]{account}));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mMockContext = null;
+ mContactsProvider = null;
+ super.tearDown();
+ }
+
+ private Contact assertLoadContact(Uri uri) {
+ final ContactLoader loader = new ContactLoader(mMockContext, uri, true);
+ return getLoaderResultSynchronously(loader);
+ }
+
+ public void testNullUri() {
+ Contact result = assertLoadContact(null);
+ assertTrue(result.isError());
+ }
+
+ public void testEmptyUri() {
+ Contact result = assertLoadContact(Uri.EMPTY);
+ assertTrue(result.isError());
+ }
+
+ public void testInvalidUri() {
+ Contact result = assertLoadContact(Uri.parse("content://wtf"));
+ assertTrue(result.isError());
+ }
+
+ public void testLoadContactWithContactIdUri() {
+ // Use content Uris that only contain the ID
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri entityUri = Uri.withAppendedPath(baseUri, Contacts.Entity.CONTENT_DIRECTORY);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ contactId);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(baseUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(baseUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithOldStyleUri() {
+ // Use content Uris that only contain the ID but use the format used in Donut
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final Uri legacyUri = ContentUris.withAppendedId(
+ Uri.parse("content://contacts"), rawContactId);
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ contactId);
+ final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, contactId, lookupKey);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(legacyUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithRawContactIdUri() {
+ // Use content Uris that only contain the ID but use the format used in Donut
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ contactId);
+ final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(rawContactUri, RawContacts.CONTENT_ITEM_TYPE);
+ queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, contactId, lookupKey);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(rawContactUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupUri() {
+ // Use lookup-style Uris that do not contain the Contact-ID
+
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupNoIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+ final Uri lookupUri = ContentUris.withAppendedId(lookupNoIdUri, contactId);
+ final Uri entityUri = Uri.withAppendedPath(lookupNoIdUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupNoIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(lookupNoIdUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupAndIdUri() {
+ // Use lookup-style Uris that also contain the Contact-ID
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ contactId);
+ final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(lookupUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+ // Use lookup-style Uris that contain incorrect Contact-ID
+ // (we want to ensure that still the correct contact is chosen)
+
+ final long contactId = 1;
+ final long wrongContactId = 2;
+ final long rawContactId = 11;
+ final long wrongRawContactId = 12;
+ final long dataId = 21;
+
+ final String lookupKey = "aa%12%@!";
+ final String wrongLookupKey = "ab%12%@!";
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ contactId);
+ final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+ wrongContactId);
+ final Uri entityUri = Uri.withAppendedPath(lookupWithWrongIdUri,
+ Contacts.Entity.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchAllData(entityUri, contactId, rawContactId, dataId, lookupKey);
+
+ Contact contact = assertLoadContact(lookupWithWrongIdUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(lookupKey, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getRawContacts().size());
+ assertEquals(1, contact.getStatuses().size());
+
+ mContactsProvider.verify();
+ }
+
+ class ContactQueries {
+ public void fetchAllData(
+ Uri baseUri, long contactId, long rawContactId, long dataId, String encodedLookup) {
+ mContactsProvider.expectQuery(baseUri)
+ .withProjection(new String[] {
+ Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID, Contacts.STARRED, Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS, Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE, Contacts.CONTACT_STATUS_LABEL,
+
+ Contacts.Entity.CONTACT_ID,
+ Contacts.Entity.RAW_CONTACT_ID,
+
+ RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET, RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
+ RawContacts.DIRTY, RawContacts.VERSION, RawContacts.SOURCE_ID,
+ RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4,
+ RawContacts.DELETED, RawContacts.NAME_VERIFIED,
+
+ Contacts.Entity.DATA_ID,
+
+ Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
+ Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10,
+ Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
+ Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4,
+ Data.DATA_VERSION, Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY, Data.MIMETYPE, Data.RES_PACKAGE,
+
+ GroupMembership.GROUP_SOURCE_ID,
+
+ Data.PRESENCE, Data.CHAT_CAPABILITY,
+ Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+ Data.STATUS_LABEL, Data.STATUS_TIMESTAMP,
+
+ Contacts.PHOTO_URI,
+
+ Contacts.SEND_TO_VOICEMAIL,
+ Contacts.CUSTOM_RINGTONE,
+ Contacts.IS_USER_PROFILE,
+ })
+ .withSortOrder(Contacts.Entity.RAW_CONTACT_ID)
+ .returnRow(
+ rawContactId, 40,
+ "aa%12%@!", "John Doe", "Doe, John", "jdo",
+ 0, 0, StatusUpdates.AVAILABLE,
+ "Having lunch", 0,
+ "mockPkg1", 10,
+
+ contactId,
+ rawContactId,
+
+ "mockAccountName", "mockAccountType", null, "mockAccountType",
+ 0, 1, 0,
+ "sync1", "sync2", "sync3", "sync4",
+ 0, 0,
+
+ dataId,
+
+ "dat1", "dat2", "dat3", "dat4", "dat5",
+ "dat6", "dat7", "dat8", "dat9", "dat10",
+ "dat11", "dat12", "dat13", "dat14", "dat15",
+ "syn1", "syn2", "syn3", "syn4",
+
+ 0, 0,
+ 0, StructuredName.CONTENT_ITEM_TYPE, "mockPkg2",
+
+ "groupId",
+
+ StatusUpdates.INVISIBLE, null,
+ "Having dinner", "mockPkg3", 0,
+ 20, 0,
+
+ "content:some.photo.uri",
+
+ 0,
+ null,
+ 0
+ );
+ }
+
+ void fetchLookupAndId(final Uri sourceUri, final long expectedContactId,
+ final String expectedEncodedLookup) {
+ mContactsProvider.expectQuery(sourceUri)
+ .withProjection(Contacts.LOOKUP_KEY, Contacts._ID)
+ .returnRow(expectedEncodedLookup, expectedContactId);
+ }
+
+ void fetchContactIdAndLookupFromRawContactUri(final Uri rawContactUri,
+ final long expectedContactId, final String expectedEncodedLookup) {
+ // TODO: use a lighter query by joining rawcontacts with contacts in provider
+ // (See ContactContracts.java)
+ final Uri dataUri = Uri.withAppendedPath(rawContactUri,
+ RawContacts.Data.CONTENT_DIRECTORY);
+ mContactsProvider.expectQuery(dataUri)
+ .withProjection(RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY)
+ .returnRow(expectedContactId, expectedEncodedLookup);
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/common/model/RawContactTest.java b/tests/src/com/android/contacts/common/model/RawContactTest.java
new file mode 100644
index 00000000..1c698c0b
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/RawContactTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 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 src.com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.contacts.common.model.RawContact;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test for {@link RawContact}.
+ */
+public class RawContactTest extends TestCase {
+
+ private RawContact buildRawContact() {
+ final ContentValues values = new ContentValues();
+ values.put("key1", "value1");
+ values.put("key2", "value2");
+
+ final ContentValues dataItem = new ContentValues();
+ dataItem.put("key3", "value3");
+ dataItem.put("key4", "value4");
+
+ final RawContact contact = new RawContact(values);
+ contact.addDataItemValues(dataItem);
+
+ return contact;
+ }
+
+ private RawContact buildRawContact2() {
+ final ContentValues values = new ContentValues();
+ values.put("key11", "value11");
+ values.put("key22", "value22");
+
+ final ContentValues dataItem = new ContentValues();
+ dataItem.put("key33", "value33");
+ dataItem.put("key44", "value44");
+
+ final RawContact contact = new RawContact(values);
+ contact.addDataItemValues(dataItem);
+
+ return contact;
+ }
+
+ public void testNotEquals() {
+ final RawContact one = buildRawContact();
+ final RawContact two = buildRawContact2();
+ assertFalse(one.equals(two));
+ }
+
+ public void testEquals() {
+ assertEquals(buildRawContact(), buildRawContact());
+ }
+
+ public void testParcelable() {
+ assertParcelableEquals(buildRawContact());
+ }
+
+ private RawContact.NamedDataItem buildNamedDataItem() {
+ final ContentValues values = new ContentValues();
+ values.put("key1", "value1");
+ values.put("key2", "value2");
+ final Uri uri = Uri.fromParts("content:", "ssp", "fragment");
+
+ return new RawContact.NamedDataItem(uri, values);
+ }
+
+ private RawContact.NamedDataItem buildNamedDataItem2() {
+ final ContentValues values = new ContentValues();
+ values.put("key11", "value11");
+ values.put("key22", "value22");
+ final Uri uri = Uri.fromParts("content:", "blah", "blah");
+
+ return new RawContact.NamedDataItem(uri, values);
+ }
+
+ public void testNamedDataItemEquals() {
+ assertEquals(buildNamedDataItem(), buildNamedDataItem());
+ }
+
+ public void testNamedDataItemNotEquals() {
+ assertFalse(buildNamedDataItem().equals(buildNamedDataItem2()));
+ }
+
+ public void testNamedDataItemParcelable() {
+ assertParcelableEquals(buildNamedDataItem());
+ }
+
+ private void assertParcelableEquals(Parcelable parcelable) {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeParcelable(parcelable, 0);
+ parcel.setDataPosition(0);
+
+ Parcelable out = parcel.readParcelable(parcelable.getClass().getClassLoader());
+ assertEquals(parcelable, out);
+ } finally {
+ parcel.recycle();
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/common/util/NameConverterTests.java b/tests/src/com/android/contacts/common/util/NameConverterTests.java
new file mode 100644
index 00000000..c4f67c3b
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/NameConverterTests.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 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 src.com.android.contacts.common.util;
+
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import com.android.contacts.common.util.NameConverter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@link NameConverter}.
+ */
+@SmallTest
+public class NameConverterTests extends AndroidTestCase {
+
+ public void testStructuredNameToDisplayName() {
+ Map<String, String> structuredName = new HashMap<String, String>();
+ structuredName.put(StructuredName.PREFIX, "Mr.");
+ structuredName.put(StructuredName.GIVEN_NAME, "John");
+ structuredName.put(StructuredName.MIDDLE_NAME, "Quincy");
+ structuredName.put(StructuredName.FAMILY_NAME, "Adams");
+ structuredName.put(StructuredName.SUFFIX, "Esquire");
+
+ assertEquals("Mr. John Quincy Adams, Esquire",
+ NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+ structuredName.remove(StructuredName.SUFFIX);
+ assertEquals("Mr. John Quincy Adams",
+ NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+ structuredName.remove(StructuredName.MIDDLE_NAME);
+ assertEquals("Mr. John Adams",
+ NameConverter.structuredNameToDisplayName(mContext, structuredName));
+ }
+
+ public void testDisplayNameToStructuredName() {
+ assertStructuredName("Mr. John Quincy Adams, Esquire",
+ "Mr.", "John", "Quincy", "Adams", "Esquire");
+ assertStructuredName("John Doe", null, "John", null, "Doe", null);
+ assertStructuredName("Ms. Jane Eyre", "Ms.", "Jane", null, "Eyre", null);
+ assertStructuredName("Dr Leo Spaceman, PhD", "Dr", "Leo", null, "Spaceman", "PhD");
+ }
+
+ /**
+ * Helper method to check whether a given display name parses out to the other parameters.
+ * @param displayName Display name to break into a structured name.
+ * @param prefix Expected prefix (null if not expected).
+ * @param givenName Expected given name (null if not expected).
+ * @param middleName Expected middle name (null if not expected).
+ * @param familyName Expected family name (null if not expected).
+ * @param suffix Expected suffix (null if not expected).
+ */
+ private void assertStructuredName(String displayName, String prefix,
+ String givenName, String middleName, String familyName, String suffix) {
+ Map<String, String> structuredName = NameConverter.displayNameToStructuredName(mContext,
+ displayName);
+ checkNameComponent(StructuredName.PREFIX, prefix, structuredName);
+ checkNameComponent(StructuredName.GIVEN_NAME, givenName, structuredName);
+ checkNameComponent(StructuredName.MIDDLE_NAME, middleName, structuredName);
+ checkNameComponent(StructuredName.FAMILY_NAME, familyName, structuredName);
+ checkNameComponent(StructuredName.SUFFIX, suffix, structuredName);
+ assertEquals(0, structuredName.size());
+ }
+
+ /**
+ * Checks that the given field and value are present in the structured name map (or not present
+ * if the given value is null). If the value is present and matches, the key is removed from
+ * the map - once all components of the name are checked, the map should be empty.
+ * @param field Field to check.
+ * @param value Expected value for the field (null if it is not expected to be populated).
+ * @param structuredName The map of structured field names to values.
+ */
+ private void checkNameComponent(String field, String value,
+ Map<String, String> structuredName) {
+ if (TextUtils.isEmpty(value)) {
+ assertNull(structuredName.get(field));
+ } else {
+ assertEquals(value, structuredName.get(field));
+ }
+ structuredName.remove(field);
+ }
+}