Merge "Import translations. DO NOT MERGE" into ub-contactsdialer-h-dev
diff --git a/proguard.flags b/proguard.flags
index 80ffe0a..e50e640 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -18,6 +18,8 @@
-keep class com.android.contacts.common.ContactPhotoManager { *; }
-keep class com.android.contacts.common.ContactsUtils { *; }
-keep class com.android.contacts.common.database.NoNullCursorAsyncQueryHandler { *; }
+-keep class com.android.contacts.common.database.SimContactDao { *; }
+-keep class com.android.contacts.common.database.SimContactDao$* { *; }
-keep class com.android.contacts.common.format.FormatUtils { *; }
-keep class com.android.contacts.common.format.TextHighlighter { *; }
-keep class com.android.contacts.common.list.ContactListItemView { *; }
diff --git a/res/anim/slide_and_fade_out.xml b/res/anim/slide_and_fade_out.xml
new file mode 100644
index 0000000..7bb65d7
--- /dev/null
+++ b/res/anim/slide_and_fade_out.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/linear_out_slow_in">
+ <alpha
+ android:duration="@integer/lists_on_load_animation_duration"
+ android:fromAlpha="1.0"
+ android:toAlpha="0"/>
+ <translate
+ android:duration="@integer/lists_on_load_animation_duration"
+ android:fromYDelta="0%"
+ android:toYDelta="5%"/>
+</set>
diff --git a/res/drawable-hdpi/ic_link_grey600_drawable_24dp.png b/res/drawable-hdpi/ic_link_grey600_drawable_24dp.png
deleted file mode 100644
index 31fcce2..0000000
--- a/res/drawable-hdpi/ic_link_grey600_drawable_24dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_link_grey600_drawable_24dp.png b/res/drawable-mdpi/ic_link_grey600_drawable_24dp.png
deleted file mode 100644
index 65997eb..0000000
--- a/res/drawable-mdpi/ic_link_grey600_drawable_24dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_link_grey600_drawable_24dp.png b/res/drawable-xhdpi/ic_link_grey600_drawable_24dp.png
deleted file mode 100644
index 9f37410..0000000
--- a/res/drawable-xhdpi/ic_link_grey600_drawable_24dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_link_grey600_drawable_24dp.png b/res/drawable-xxhdpi/ic_link_grey600_drawable_24dp.png
deleted file mode 100644
index 140daef..0000000
--- a/res/drawable-xxhdpi/ic_link_grey600_drawable_24dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_link_grey600_drawable_24dp.png b/res/drawable-xxxhdpi/ic_link_grey600_drawable_24dp.png
deleted file mode 100644
index 6fe9b8c..0000000
--- a/res/drawable-xxxhdpi/ic_link_grey600_drawable_24dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_assistant.xml b/res/drawable/ic_assistant.xml
new file mode 100644
index 0000000..3391beb
--- /dev/null
+++ b/res/drawable/ic_assistant.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,2L5,2c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h4l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM13.88,12.88L12,17l-1.88,-4.12L6,11l4.12,-1.88L12,5l1.88,4.12L18,11l-4.12,1.88z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_group_add.xml b/res/drawable/ic_group_add.xml
new file mode 100644
index 0000000..6d8776e
--- /dev/null
+++ b/res/drawable/ic_group_add.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M8,10L5,10L5,7L3,7v3L0,10v2h3v3h2v-3h3v-2zM18,11c1.66,0 2.99,-1.34 2.99,-3S19.66,5 18,5c-0.32,0 -0.63,0.05 -0.91,0.14 0.57,0.81 0.9,1.79 0.9,2.86s-0.34,2.04 -0.9,2.86c0.28,0.09 0.59,0.14 0.91,0.14zM13,11c1.66,0 2.99,-1.34 2.99,-3S14.66,5 13,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM19.62,13.16c0.83,0.73 1.38,1.66 1.38,2.84v2h3v-2c0,-1.54 -2.37,-2.49 -4.38,-2.84zM13,13c-2,0 -6,1 -6,3v2h12v-2c0,-2 -4,-3 -6,-3z"/>
+</vector>
\ No newline at end of file
diff --git a/res/layout-land/contact_editor_fragment.xml b/res/layout-land/contact_editor_fragment.xml
index 6b02462..7f98765 100644
--- a/res/layout-land/contact_editor_fragment.xml
+++ b/res/layout-land/contact_editor_fragment.xml
@@ -44,8 +44,6 @@
android:layout_height="wrap_content"
android:orientation="vertical">
- <include layout="@layout/editor_all_rawcontacts_accounts_selector" />
-
<include layout="@layout/editor_account_header" />
<include layout="@layout/contact_editor_fields" />
diff --git a/res/layout/contact_editor_fragment.xml b/res/layout/contact_editor_fragment.xml
index 85c4cf1..690be8c 100644
--- a/res/layout/contact_editor_fragment.xml
+++ b/res/layout/contact_editor_fragment.xml
@@ -39,7 +39,6 @@
android:focusable="true"
android:focusableInTouchMode="true"/>
- <include layout="@layout/editor_all_rawcontacts_accounts_selector" />
<include layout="@layout/editor_account_header" />
<include layout="@layout/contact_editor_fields" />
diff --git a/res/layout/editor_all_rawcontacts_accounts_selector.xml b/res/layout/editor_all_rawcontacts_accounts_selector.xml
deleted file mode 100644
index 5606422..0000000
--- a/res/layout/editor_all_rawcontacts_accounts_selector.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<!-- Header at the top of a raw contact editor. This allows users to change the account that
- the raw contact is saved in. -->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/all_rawcontacts_accounts_container"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:minHeight="@dimen/editor_min_line_item_height"
- android:orientation="horizontal"
- android:visibility="gone" >
-
- <ImageView
- android:src="@drawable/ic_link_grey600_drawable_24dp"
- android:tint="@color/editor_icon_color"
- style="@style/EditSelectorIconStyle"/>
-
- <TextView
- android:id="@+id/rawcontacts_accounts_summary"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginStart="@dimen/editor_account_left_margin"
- android:textSize="16sp"
- android:singleLine="true"
- android:layout_weight="1"
- android:textColor="@color/primary_text_color"
- android:textAlignment="viewStart"
- android:layout_gravity="center_vertical"
- android:gravity="center_vertical"
- android:ellipsize="end" />
-
- <ImageView
- android:src="@drawable/ic_menu_expand_minimized_24dp"
- android:tint="@color/quantum_black_secondary_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical|end"
- android:layout_alignParentEnd="true"
- android:paddingStart="@dimen/editor_round_button_padding_left"
- android:paddingEnd="@dimen/editor_round_button_padding_right"
- android:paddingTop="@dimen/editor_round_button_padding_top"
- android:paddingBottom="@dimen/editor_round_button_padding_bottom"/>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
new file mode 100644
index 0000000..9e2f493
--- /dev/null
+++ b/res/layout/fragment_sim_import.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="4dp"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ app:navigationIcon="@drawable/ic_close_dk"
+ app:title="@string/sim_import_title_none_selected">
+
+ <Button
+ android:id="@+id/import_button"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:text="@string/sim_import_button_text"
+ />
+ </android.support.v7.widget.Toolbar>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:background="?android:colorBackground"
+ android:elevation="4dp">
+
+ <include layout="@layout/editor_account_header"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <android.support.v4.widget.ContentLoadingProgressBar
+ android:id="@+id/loading_progress"
+ style="@style/Widget.AppCompat.ProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"/>
+
+ <TextView
+ android:id="@+id/empty_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/sim_import_empty_message"
+ android:textAppearance="?android:textAppearanceMedium"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/menu/activity_main_drawer.xml b/res/menu/activity_main_drawer.xml
index a006761..4082f5f 100644
--- a/res/menu/activity_main_drawer.xml
+++ b/res/menu/activity_main_drawer.xml
@@ -24,9 +24,9 @@
android:icon="@drawable/ic_menu_filter"
android:title="@string/contactsList" />
<item
- android:id="@+id/nav_find_duplicates"
- android:icon="@drawable/ic_menu_duplicates"
- android:title="@string/menu_duplicates"/>
+ android:id="@+id/nav_assistant"
+ android:icon="@drawable/ic_assistant"
+ android:title="@string/menu_assistant"/>
</group>
<group android:id="@+id/groups">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 454777e..e5d6413 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -899,8 +899,8 @@
<!-- Text shown in the contacts app while the background process updates contacts after a locale change [CHAR LIMIT=150]-->
<string name="locale_change_in_progress">Contact list is being updated to reflect the change of language.\n\nPlease wait…</string>
- <!-- The menu item to open the link/merge duplicates activity. [CHAR LIMIT=20]-->
- <string name="menu_duplicates">Duplicates</string>
+ <!-- The menu item to open the Google contacts assistant. [CHAR LIMIT=20]-->
+ <string name="menu_assistant">Assistant</string>
<!-- Open drawer content descriptions [CHAR LIMIT=40] -->
<string name="navigation_drawer_open">Open navigation drawer</string>
@@ -1787,4 +1787,26 @@
<!-- Toast shown when a dynamic shortcut is tapped after being disabled because the contact was removed -->
<string name="dynamic_shortcut_contact_removed_message">Contact was removed</string>
+ <!-- Text for button shown in toolbar to start import of SIM contacts -->
+ <string name="sim_import_button_text">Import</string>
+
+ <!-- Toolbar title shown when importing SIM contacts and none are selected -->
+ <string name="sim_import_title_none_selected">Select contacts</string>
+
+ <!-- Toolbar title shown when importing SIM contacts and some are selected -->
+ <string name="sim_import_title_some_selected_fmt"><xliff:g id="count">%d</xliff:g> Selected</string>
+
+ <!-- Message shown when the SIM import screen is displayed but there are no contacts on the
+ SIM card -->
+ <string name="sim_import_empty_message">No contacts on your SIM card</string>
+
+ <!-- Toast shown on settings screen when importing from SIM completes successfully -->
+ <plurals name="sim_import_success_toast_fmt">
+ <item quantity="one">1 SIM contact imported</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> SIM contacts imported</item>
+ </plurals>
+
+ <!-- Toast shown on settings screen when importing from SIM completes with an error -->
+ <string name="sim_import_failed_toast">Failed to import SIM contacts</string>
+
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d51392a..fc52bc5 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -253,7 +253,7 @@
<style name="ContactPickerTheme" parent="PeopleActivityTheme" >
</style>
- <style name="ContactsPreferencesTheme" parent="@style/PeopleTheme">
+ <style name="ContactsPreferencesTheme" parent="@style/PeopleThemeAppCompat">
<item name="android:listViewStyle">@style/ListViewStyle</item>
</style>
@@ -508,4 +508,18 @@
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
+
+ <style name="FullScreenDialogAnimationStyle">
+ <item name="android:windowEnterAnimation">@anim/slide_and_fade_in</item>
+ <item name="android:windowExitAnimation">@anim/slide_and_fade_out</item>
+ </style>
+
+ <style name="PeopleThemeAppCompat.FullScreenDialog">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ <item name="android:listSelector">?android:attr/listChoiceBackgroundIndicator</item>
+ <item name="android:windowAnimationStyle">@style/FullScreenDialogAnimationStyle</item>
+ </style>
</resources>
diff --git a/src-bind/com/android/contactsbind/ObjectFactory.java b/src-bind/com/android/contactsbind/ObjectFactory.java
index 83b8f4d..e336e4f 100644
--- a/src-bind/com/android/contactsbind/ObjectFactory.java
+++ b/src-bind/com/android/contactsbind/ObjectFactory.java
@@ -42,11 +42,7 @@
return new DeviceLocalAccountTypeFactory.Default(context);
}
- public static Fragment getDuplicatesFragment() {
- return null;
- }
-
- public static Fragment getDuplicatesUtilFragment() {
+ public static Fragment getAssistantFragment() {
return null;
}
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index d4da588..1ed36b5 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -16,7 +16,6 @@
package com.android.contacts;
-import static android.Manifest.permission.WRITE_CONTACTS;
import android.app.Activity;
import android.app.IntentService;
import android.content.ContentProviderOperation;
@@ -54,16 +53,17 @@
import com.android.contacts.activities.ContactEditorActivity;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.database.ContactUpdateUtils;
+import com.android.contacts.common.database.SimContactDao;
import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.CPOWrapper;
import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.RawContactDeltaList;
import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.compat.PinnedPositionsCompat;
import com.android.contacts.util.ContactPhotoUtils;
-
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -72,6 +72,8 @@
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
+import static android.Manifest.permission.WRITE_CONTACTS;
+
/**
* A service responsible for saving changes to the content provider.
*/
@@ -86,6 +88,7 @@
public static final String EXTRA_ACCOUNT_NAME = "accountName";
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
public static final String EXTRA_DATA_SET = "dataSet";
+ public static final String EXTRA_ACCOUNT = "account";
public static final String EXTRA_CONTENT_VALUES = "contentValues";
public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
@@ -136,12 +139,23 @@
public static final String EXTRA_UNDO_ACTION = "undoAction";
public static final String EXTRA_UNDO_DATA = "undoData";
- public static final String BROADCAST_ACTION_GROUP_DELETED = "groupDeleted";
+ public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
+ public static final String EXTRA_SIM_CONTACTS = "simContacts";
+
+ public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
+ public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
+
+ public static final String EXTRA_RESULT_CODE = "resultCode";
+ public static final String EXTRA_RESULT_COUNT = "count";
+ public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
public static final int CP2_ERROR = 0;
public static final int CONTACTS_LINKED = 1;
public static final int CONTACTS_SPLIT = 2;
public static final int BAD_ARGUMENTS = 3;
+ public static final int RESULT_UNKNOWN = 0;
+ public static final int RESULT_SUCCESS = 1;
+ public static final int RESULT_FAILURE = 2;
private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
Data.MIMETYPE,
@@ -176,6 +190,7 @@
private Handler mMainHandler;
private GroupsDao mGroupsDao;
+ private SimContactDao mSimContactDao;
public ContactSaveService() {
super(TAG);
@@ -187,6 +202,7 @@
public void onCreate() {
super.onCreate();
mGroupsDao = new GroupsDaoImpl(this);
+ mSimContactDao = new SimContactDao(this);
}
public static void registerListener(Listener listener) {
@@ -305,6 +321,8 @@
setRingtone(intent);
} else if (ACTION_UNDO.equals(action)) {
undo(intent);
+ } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
+ importFromSim(intent);
}
}
@@ -812,7 +830,7 @@
}
final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
- final Intent callbackIntent = new Intent(BROADCAST_ACTION_GROUP_DELETED);
+ final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
@@ -1628,6 +1646,36 @@
operations.add(builder.build());
}
+ public static Intent createImportFromSimIntent(Context context,
+ ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
+ return new Intent(context, ContactSaveService.class)
+ .setAction(ACTION_IMPORT_FROM_SIM)
+ .putExtra(EXTRA_SIM_CONTACTS, contacts)
+ .putExtra(EXTRA_ACCOUNT, targetAccount);
+ }
+
+ private void importFromSim(Intent intent) {
+ final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
+ .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
+ try {
+ final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
+ final ArrayList<SimContact> contacts =
+ intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
+ mSimContactDao.importContacts(contacts, targetAccount);
+ // notify success
+ LocalBroadcastManager.getInstance(this).sendBroadcast(result
+ .putExtra(EXTRA_RESULT_COUNT, contacts.size())
+ .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS));
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "importFromSim completed successfully");
+ }
+ } catch (RemoteException|OperationApplicationException e) {
+ Log.e(TAG, "Failed to import contacts from SIM card", e);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(result
+ .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
+ }
+ }
+
/**
* Shows a toast on the UI thread.
*/
diff --git a/src/com/android/contacts/ContactsDrawerActivity.java b/src/com/android/contacts/ContactsDrawerActivity.java
index 3fc3c9d..68b9884 100644
--- a/src/com/android/contacts/ContactsDrawerActivity.java
+++ b/src/com/android/contacts/ContactsDrawerActivity.java
@@ -20,13 +20,13 @@
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.provider.ContactsContract.Intents;
import android.support.annotation.LayoutRes;
import android.support.design.widget.NavigationView;
-import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
@@ -49,6 +49,8 @@
import com.android.contacts.common.list.ContactListFilter;
import com.android.contacts.common.list.ContactListFilterController;
import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountDisplayInfo;
+import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.preference.ContactsPreferenceActivity;
import com.android.contacts.common.util.AccountFilterUtil;
@@ -69,8 +71,6 @@
import com.android.contacts.interactions.AccountFiltersFragment.AccountFiltersListener;
import com.android.contacts.list.DefaultContactBrowseListFragment;
import com.android.contacts.list.MultiSelectContactsListFragment;
-import com.android.contacts.common.model.account.AccountDisplayInfo;
-import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
import com.android.contacts.util.SharedPreferenceUtil;
import com.android.contactsbind.HelpUtils;
import com.android.contactsbind.ObjectFactory;
@@ -94,7 +94,7 @@
public enum ContactsView {
NONE,
ALL_CONTACTS,
- DUPLICATES,
+ ASSISTANT,
GROUP_VIEW,
ACCOUNT_VIEW,
}
@@ -223,8 +223,15 @@
mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
mToggle = new ContactsActionBarDrawerToggle(this, mDrawer, mToolbar,
R.string.navigation_drawer_open, R.string.navigation_drawer_close);
+
mDrawer.setDrawerListener(mToggle);
- mToggle.syncState();
+ // Set fallback handler for when drawer is disabled.
+ mToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
// Set up navigation mode.
if (savedState != null) {
@@ -246,16 +253,32 @@
}
}
+ public void setDrawerLockMode(boolean enabled) {
+ // Prevent drawer from being opened by sliding from the start of screen.
+ mDrawer.setDrawerLockMode(enabled ? DrawerLayout.LOCK_MODE_UNLOCKED
+ : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+
+ // Order of these statements matter.
+ // Display back button and disable drawer indicator.
+ if (enabled) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(false);
+ mToggle.setDrawerIndicatorEnabled(true);
+ } else {
+ mToggle.setDrawerIndicatorEnabled(false);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
private void setUpMenu() {
final Menu menu = mNavigationView.getMenu();
- if (ObjectFactory.getDuplicatesFragment() == null) {
- menu.removeItem(R.id.nav_find_duplicates);
+ if (ObjectFactory.getAssistantFragment() == null) {
+ menu.removeItem(R.id.nav_assistant);
} else {
- final MenuItem findDupMenu = menu.findItem(R.id.nav_find_duplicates);
- mIdMenuMap.put(R.id.nav_find_duplicates, findDupMenu);
- if (isDuplicatesView()) {
- updateMenuSelection(findDupMenu);
+ final MenuItem assistantMenu = menu.findItem(R.id.nav_assistant);
+ mIdMenuMap.put(R.id.nav_assistant, assistantMenu);
+ if (isAssistantView()) {
+ updateMenuSelection(assistantMenu);
}
}
@@ -306,6 +329,18 @@
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mToggle.syncState();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mToggle.onConfigurationChanged(newConfig);
+ }
+
// Set up fragment manager to load groups and filters.
protected void loadGroupsAndFilters() {
final FragmentManager fragmentManager = getFragmentManager();
@@ -429,8 +464,8 @@
return mCurrentView == ContactsView.GROUP_VIEW;
}
- protected boolean isDuplicatesView() {
- return mCurrentView == ContactsView.DUPLICATES;
+ protected boolean isAssistantView() {
+ return mCurrentView == ContactsView.ASSISTANT;
}
protected boolean isAllContactsView() {
@@ -442,7 +477,7 @@
}
public boolean isInSecondLevel() {
- return isGroupView() || isDuplicatesView();
+ return isGroupView() || isAssistantView();
}
protected abstract void onGroupMenuItemClicked(long groupId, String title);
@@ -556,9 +591,9 @@
HelpUtils.launchHelpAndFeedbackForMainScreen(ContactsDrawerActivity.this);
} else if (id == R.id.nav_all_contacts) {
switchToAllContacts();
- } else if (id == R.id.nav_find_duplicates) {
- if (!isDuplicatesView()) {
- launchFindDuplicates();
+ } else if (id == R.id.nav_assistant) {
+ if (!isAssistantView()) {
+ launchAssistant();
updateMenuSelection(item);
}
} else if (item.getIntent() != null) {
@@ -599,7 +634,7 @@
mContactListFilterController, AppCompatActivity.RESULT_OK, intent);
}
- protected abstract void launchFindDuplicates();
+ protected abstract void launchAssistant();
protected abstract DefaultContactBrowseListFragment getAllFragment();
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
new file mode 100644
index 0000000..a9dcf97
--- /dev/null
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts;
+
+import android.app.DialogFragment;
+import android.app.LoaderManager;
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.widget.ContentLoadingProgressBar;
+import android.support.v7.widget.Toolbar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.list.ContactListAdapter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.editor.AccountHeaderPresenter;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Dialog that presents a list of contacts from a SIM card that can be imported into a selected
+ * account
+ */
+public class SimImportFragment extends DialogFragment
+ implements LoaderManager.LoaderCallbacks<ArrayList<SimContact>>,
+ MultiSelectEntryContactListAdapter.SelectedContactsListener {
+
+ private static final String KEY_ACCOUNT = "account";
+ private static final String ARG_SUBSCRIPTION_ID = "subscriptionId";
+ public static final int NO_SUBSCRIPTION_ID = -1;
+
+ private ContactsPreferences mPreferences;
+ private AccountTypeManager mAccountTypeManager;
+ private SimContactAdapter mAdapter;
+ private AccountHeaderPresenter mAccountHeaderPresenter;
+ private ContentLoadingProgressBar mLoadingIndicator;
+ private Toolbar mToolbar;
+ private ListView mListView;
+ private View mImportButton;
+
+ private int mSubscriptionId;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(STYLE_NO_TITLE, R.style.PeopleThemeAppCompat_FullScreenDialog);
+ mPreferences = new ContactsPreferences(getContext());
+ mAccountTypeManager = AccountTypeManager.getInstance(getActivity());
+ mAdapter = new SimContactAdapter(getActivity());
+
+ mAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity()));
+ mAdapter.setDisplayCheckBoxes(true);
+ mAdapter.setHasHeader(0, false);
+
+ final Bundle args = getArguments();
+ mSubscriptionId = args == null ? NO_SUBSCRIPTION_ID : args.getInt(ARG_SUBSCRIPTION_ID,
+ NO_SUBSCRIPTION_ID);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.fragment_sim_import, container, false);
+
+ mAccountHeaderPresenter = new AccountHeaderPresenter(
+ view.findViewById(R.id.account_header_container));
+ if (savedInstanceState != null) {
+ AccountWithDataSet account = savedInstanceState.getParcelable(KEY_ACCOUNT);
+ mAccountHeaderPresenter.setCurrentAccount(account);
+ } else {
+ final AccountWithDataSet currentDefaultAccount = AccountWithDataSet
+ .getDefaultOrBestFallback(mPreferences, mAccountTypeManager);
+ mAccountHeaderPresenter.setCurrentAccount(currentDefaultAccount);
+ }
+
+ mListView = (ListView) view.findViewById(R.id.list);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ mAdapter.toggleSelectionOfContactId(id);
+ }
+ });
+ mImportButton = view.findViewById(R.id.import_button);
+ mImportButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ importCurrentSelections();
+ // Do we wait for import to finish?
+ dismiss();
+ }
+ });
+ mImportButton.setEnabled(mAdapter.getSelectedContactIds().size() > 0);
+
+ mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+
+ mLoadingIndicator = (ContentLoadingProgressBar) view.findViewById(R.id.loading_progress);
+ mAdapter.setSelectedContactsListener(this);
+
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mAdapter.isEmpty() && getLoaderManager().getLoader(0).isStarted()) {
+ mLoadingIndicator.show();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_ACCOUNT, mAccountHeaderPresenter.getCurrentAccount());
+ }
+
+ @Override
+ public SimContactLoader onCreateLoader(int id, Bundle args) {
+ return new SimContactLoader(getContext(), mSubscriptionId);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ArrayList<SimContact>> loader,
+ ArrayList<SimContact> data) {
+ mListView.setEmptyView(getView().findViewById(R.id.empty_message));
+ mAdapter.setContacts(data);
+ // we default to selecting all contacts.
+ mAdapter.selectAll();
+ mLoadingIndicator.hide();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ArrayList<SimContact>> loader) {
+ }
+
+ private void importCurrentSelections() {
+ ContactSaveService.startService(getContext(), ContactSaveService
+ .createImportFromSimIntent(getContext(), mAdapter.getSelectedContacts(),
+ mAccountHeaderPresenter.getCurrentAccount()));
+ }
+
+ @Override
+ public void onSelectedContactsChanged() {
+ updateSelectedCount();
+ }
+
+ @Override
+ public void onSelectedContactsChangedViaCheckBox() {
+ updateSelectedCount();
+ }
+
+ private void updateSelectedCount() {
+ final int selectedCount = mAdapter.getSelectedContactIds().size();
+ if (selectedCount == 0) {
+ mToolbar.setTitle(R.string.sim_import_title_none_selected);
+ } else {
+ mToolbar.setTitle(getString(R.string.sim_import_title_some_selected_fmt,
+ selectedCount));
+ }
+ if (mImportButton != null) {
+ mImportButton.setEnabled(selectedCount > 0);
+ }
+ }
+
+ public Context getContext() {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return super.getContext();
+ }
+ return getActivity();
+ }
+
+ /**
+ * Creates a fragment that will display contacts stored on the default SIM card
+ */
+ public static SimImportFragment newInstance() {
+ return new SimImportFragment();
+ }
+
+ /**
+ * Creates a fragment that will display the contacts stored on the SIM card that has the
+ * provided subscriptionId
+ */
+ public static SimImportFragment newInstance(int subscriptionId) {
+ final SimImportFragment fragment = new SimImportFragment();
+ final Bundle args = new Bundle();
+ args.putInt(ARG_SUBSCRIPTION_ID, subscriptionId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ private static class SimContactAdapter extends ContactListAdapter {
+ private ArrayList<SimContact> mContacts;
+
+ public SimContactAdapter(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ ContactListItemView contactView = (ContactListItemView) itemView;
+ bindNameAndViewId(contactView, cursor);
+ bindPhoto(contactView, partition, cursor);
+ }
+
+ public void setContacts(ArrayList<SimContact> contacts) {
+ mContacts = contacts;
+ changeCursor(SimContact.convertToContactsCursor(mContacts,
+ ContactQuery.CONTACT_PROJECTION_PRIMARY));
+ }
+
+ public ArrayList<SimContact> getSelectedContacts() {
+ if (mContacts == null) return null;
+
+ final Set<Long> selectedIds = getSelectedContactIds();
+ final ArrayList<SimContact> selected = new ArrayList<>();
+ for (SimContact contact : mContacts) {
+ if (selectedIds.contains(contact.getId())) {
+ selected.add(contact);
+ }
+ }
+ return selected;
+ }
+
+ public void selectAll() {
+ if (mContacts == null) return;
+
+ final TreeSet<Long> selected = new TreeSet<>();
+ for (SimContact contact : mContacts) {
+ selected.add(contact.getId());
+ }
+ setSelectedContactIds(selected);
+ }
+ }
+
+ public static class SimContactLoader extends AsyncTaskLoader<ArrayList<SimContact>> {
+ private SimContactDao mDao;
+ private final int mSubscriptionId;
+ private ArrayList<SimContact> mData;
+
+ public SimContactLoader(Context context, int subscriptionId) {
+ super(context);
+ mDao = new SimContactDao(context);
+ mSubscriptionId = subscriptionId;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mData != null) {
+ deliverResult(mData);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void deliverResult(ArrayList<SimContact> data) {
+ mData = data;
+ super.deliverResult(data);
+ }
+
+ @Override
+ public ArrayList<SimContact> loadInBackground() {
+ if (mSubscriptionId != NO_SUBSCRIPTION_ID) {
+ return mDao.loadSimContacts(mSubscriptionId);
+ } else {
+ return mDao.loadSimContacts();
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ mData = null;
+ }
+ }
+}
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index 5022947..1845a52 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -288,9 +288,6 @@
} else {
startActivityForResult(resultIntent, /* requestCode */ 0);
}
-
- ImplicitIntentsUtil.startActivityInApp(
- ContactEditorActivity.this, resultIntent);
}
finish();
}
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index b2a0a07..5621c1b 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -20,15 +20,13 @@
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
-import android.content.ContentResolver;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
-import android.content.SyncStatusObserver;
import android.content.IntentFilter;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
+import android.content.SyncStatusObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -40,7 +38,6 @@
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.SwipeRefreshLayout;
-import android.text.TextUtils;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -65,8 +62,6 @@
import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.logging.ScreenEvent.ScreenType;
import com.android.contacts.common.model.AccountTypeManager;
-import com.android.contacts.common.model.account.AccountDisplayInfo;
-import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.util.AccountFilterUtil;
import com.android.contacts.common.util.Constants;
@@ -96,9 +91,11 @@
private static final String TAG_ALL = "contacts-all";
private static final String TAG_UNAVAILABLE = "contacts-unavailable";
private static final String TAG_GROUP_VIEW = "contacts-groups";
- private static final String TAG_DUPLICATES = "contacts-duplicates";
- private static final String TAG_SECOND_LEVEL = "second-level";
- // Tag for DuplicatesUtilFragment.java
+ public static final String TAG_ASSISTANT = "contacts-assistant";
+ public static final String TAG_SECOND_LEVEL = "second-level";
+ public static final String TAG_THIRD_LEVEL = "third-level";
+
+ public static final String TAG_DUPLICATES = "DuplicatesFragment";
public static final String TAG_DUPLICATES_UTIL = "DuplicatesUtilFragment";
private static final String KEY_GROUP_URI = "groupUri";
@@ -469,7 +466,7 @@
mSaveServiceListener = new SaveServiceListener();
LocalBroadcastManager.getInstance(this).registerReceiver(mSaveServiceListener,
- new IntentFilter(ContactSaveService.BROADCAST_ACTION_GROUP_DELETED));
+ new IntentFilter(ContactSaveService.BROADCAST_GROUP_DELETED));
}
@Override
@@ -631,8 +628,8 @@
return;
}
- if (isDuplicatesView()) {
- switchToAllContacts();
+ if (isAssistantView()) {
+ onBackPressedAssistantView();
return;
}
@@ -662,6 +659,14 @@
}
}
+ private void onBackPressedAssistantView() {
+ if (!popThirdLevel()) {
+ switchToAllContacts();
+ } else {
+ setDrawerLockMode(/* enabled */ true);
+ }
+ }
+
// Returns true if back event is handled in this method.
private boolean maybeHandleInAllFragment() {
if (isAllFragmentInSelectionMode()) {
@@ -731,7 +736,7 @@
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
- case ContactSaveService.BROADCAST_ACTION_GROUP_DELETED:
+ case ContactSaveService.BROADCAST_GROUP_DELETED:
onGroupDeleted(intent);
break;
}
@@ -769,8 +774,8 @@
}
@Override
- protected void launchFindDuplicates() {
- switchView(ContactsView.DUPLICATES);
+ protected void launchAssistant() {
+ switchView(ContactsView.ASSISTANT);
}
private void switchView(ContactsView contactsView) {
@@ -784,20 +789,14 @@
}
transaction.replace(
R.id.contacts_list_container, mMembersFragment, TAG_GROUP_VIEW);
- } else if (isDuplicatesView()) {
- Fragment duplicatesFragment = fragmentManager.findFragmentByTag(TAG_DUPLICATES);
- Fragment duplicatesUtilFragment =
- fragmentManager.findFragmentByTag(TAG_DUPLICATES_UTIL);
- if (duplicatesFragment == null || duplicatesUtilFragment == null) {
- duplicatesFragment = ObjectFactory.getDuplicatesFragment();
- duplicatesUtilFragment = ObjectFactory.getDuplicatesUtilFragment();
- duplicatesUtilFragment.setTargetFragment(duplicatesFragment, /* requestCode */ 0);
+ } else if(isAssistantView()) {
+ Fragment assistantFragment = fragmentManager.findFragmentByTag(TAG_ASSISTANT);
+ if (assistantFragment == null) {
+ assistantFragment = ObjectFactory.getAssistantFragment();
}
- transaction.replace(
- R.id.contacts_list_container, duplicatesFragment, TAG_DUPLICATES);
- if (!duplicatesUtilFragment.isAdded()) {
- transaction.add(duplicatesUtilFragment, TAG_DUPLICATES_UTIL);
- resetToolBarStatusBarColor();
+ if (assistantFragment != null) {
+ transaction.replace(
+ R.id.contacts_list_container, assistantFragment, TAG_ASSISTANT);
}
resetToolBarStatusBarColor();
}
@@ -821,6 +820,11 @@
super.switchToAllContacts();
}
+ private boolean popThirdLevel() {
+ return getFragmentManager().popBackStackImmediate(
+ TAG_THIRD_LEVEL, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+
private void popSecondLevel() {
getFragmentManager().popBackStackImmediate(
TAG_SECOND_LEVEL, FragmentManager.POP_BACK_STACK_INCLUSIVE);
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
new file mode 100644
index 0000000..f9fb4e8
--- /dev/null
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.database;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides data access methods for loading contacts from a SIM card and and migrating these
+ * SIM contacts to a CP2 account.
+ */
+public class SimContactDao {
+ @VisibleForTesting
+ public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
+
+ public static String _ID = BaseColumns._ID;
+ public static String NAME = "name";
+ public static String NUMBER = "number";
+ public static String EMAILS = "emails";
+
+ private ContentResolver mResolver;
+
+ public SimContactDao(Context context) {
+ this(context.getContentResolver());
+ }
+
+ private SimContactDao(ContentResolver resolver) {
+ mResolver = resolver;
+ }
+
+ public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
+ return loadFrom(ICC_CONTENT_URI.buildUpon()
+ .appendPath("subId")
+ .appendPath(String.valueOf(subscriptionId))
+ .build());
+ }
+
+ public ArrayList<SimContact> loadSimContacts() {
+ return loadFrom(ICC_CONTENT_URI);
+ }
+
+ private ArrayList<SimContact> loadFrom(Uri uri) {
+ final Cursor cursor = mResolver.query(uri, null, null, null, null);
+
+ try {
+ return loadFromCursor(cursor);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
+ final int colId = cursor.getColumnIndex(_ID);
+ final int colName = cursor.getColumnIndex(NAME);
+ final int colNumber = cursor.getColumnIndex(NUMBER);
+ final int colEmails = cursor.getColumnIndex(EMAILS);
+
+ final ArrayList<SimContact> result = new ArrayList<>();
+
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(colId);
+ final String name = cursor.getString(colName);
+ final String number = cursor.getString(colNumber);
+ final String emails = cursor.getString(colEmails);
+
+ final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
+ result.add(contact);
+ }
+ return result;
+ }
+
+ public ContentProviderResult[] importContacts(List<SimContact> contacts,
+ AccountWithDataSet targetAccount)
+ throws RemoteException, OperationApplicationException {
+ final ArrayList<ContentProviderOperation> ops = createImportOperations(contacts, targetAccount);
+ return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
+ }
+
+ private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
+ AccountWithDataSet targetAccount) {
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (SimContact contact : contacts) {
+ contact.appendCreateContactOperations(ops, targetAccount);
+ }
+ return ops;
+ }
+
+ private String[] parseEmails(String emails) {
+ return emails != null ? emails.split(",") : null;
+ }
+}
diff --git a/src/com/android/contacts/common/interactions/ImportDialogFragment.java b/src/com/android/contacts/common/interactions/ImportDialogFragment.java
index 0c0ce73..991a9c2 100644
--- a/src/com/android/contacts/common/interactions/ImportDialogFragment.java
+++ b/src/com/android/contacts/common/interactions/ImportDialogFragment.java
@@ -37,6 +37,7 @@
import android.widget.ArrayAdapter;
import android.widget.TextView;
+import com.android.contacts.SimImportFragment;
import com.android.contacts.common.R;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
@@ -151,7 +152,9 @@
public void onClick(DialogInterface dialog, int which) {
boolean dismissDialog;
final int resId = adapter.getItem(which).mChoiceResourceId;
- if (resId == R.string.import_from_sim || resId == R.string.import_from_vcf_file) {
+ if (resId == R.string.import_from_sim) {
+ dismissDialog = handleSimImportRequest(adapter.getItem(which).mSubscriptionId);
+ } else if (resId == R.string.import_from_vcf_file) {
dismissDialog = handleImportRequest(resId,
adapter.getItem(which).mSubscriptionId);
} else {
@@ -172,8 +175,13 @@
.create();
}
+ private boolean handleSimImportRequest(int subscriptionId) {
+ SimImportFragment.newInstance(subscriptionId).show(getFragmentManager(), "SimImport");
+ return true;
+ }
+
/**
- * Handle "import from SIM" and "import from SD".
+ * Handle "import from SD".
*
* @return {@code true} if the dialog show be closed. {@code false} otherwise.
*/
diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java
index 677aa46..6294f47 100644
--- a/src/com/android/contacts/common/list/ContactListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -41,7 +41,7 @@
public abstract class ContactListAdapter extends MultiSelectEntryContactListAdapter {
public static class ContactQuery {
- private static final String[] CONTACT_PROJECTION_PRIMARY = new String[] {
+ public static final String[] CONTACT_PROJECTION_PRIMARY = new String[] {
Contacts._ID, // 0
Contacts.DISPLAY_NAME_PRIMARY, // 1
Contacts.CONTACT_PRESENCE, // 2
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
new file mode 100644
index 0000000..a13ef67
--- /dev/null
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Holds data for contacts loaded from the SIM card.
+ */
+public class SimContact implements Parcelable {
+ private final long mId;
+ private final String mName;
+ private final String mPhone;
+ private final String[] mEmails;
+
+ public SimContact(long id, String name, String phone, String[] emails) {
+ this.mId = id;
+ this.mName = name;
+ this.mPhone = phone;
+ this.mEmails = emails;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getPhone() {
+ return mPhone;
+ }
+
+ public String[] getEmails() {
+ return mEmails;
+ }
+
+ public void appendCreateContactOperations(List<ContentProviderOperation> ops,
+ AccountWithDataSet targetAccount) {
+ // nothing to save.
+ if (mName == null && mPhone == null && mEmails == null) return;
+
+ final int rawContactOpIndex = ops.size();
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withYieldAllowed(true)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, targetAccount.name)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, targetAccount.type)
+ .withValue(ContactsContract.RawContacts.DATA_SET, targetAccount.dataSet)
+ .build());
+ if (mName != null) {
+ ops.add(createInsertOp(rawContactOpIndex, StructuredName.CONTENT_ITEM_TYPE,
+ StructuredName.DISPLAY_NAME, mName));
+ }
+ if (mPhone != null) {
+ ops.add(createInsertOp(rawContactOpIndex, Phone.CONTENT_ITEM_TYPE,
+ Phone.NUMBER, mPhone));
+ }
+ if (mEmails != null) {
+ for (String email : mEmails) {
+ ops.add(createInsertOp(rawContactOpIndex, Email.CONTENT_ITEM_TYPE,
+ Email.ADDRESS, email));
+ }
+ }
+ }
+
+ private ContentProviderOperation createInsertOp(int rawContactOpIndex, String mimeType,
+ String column, String value) {
+ return ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactOpIndex)
+ .withValue(ContactsContract.Data.MIMETYPE, mimeType)
+ .withValue(column, value)
+ .build();
+ }
+
+ public void appendAsContactRow(MatrixCursor cursor) {
+ cursor.newRow().add(ContactsContract.Contacts._ID, mId)
+ .add(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, mName)
+ .add(ContactsContract.Contacts.LOOKUP_KEY, getLookupKey());
+ }
+
+ /**
+ * Generate a "fake" lookup key. This is needed because
+ * {@link com.android.contacts.common.ContactPhotoManager} will only generate a letter avatar
+ * if the contact has a lookup key.
+ */
+ private String getLookupKey() {
+ if (mName != null) {
+ return "sim-n-" + Uri.encode(mName);
+ } else if (mPhone != null) {
+ return "sim-p-" + Uri.encode(mPhone);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SimContact{" +
+ "mId=" + mId +
+ ", mName='" + mName + '\'' +
+ ", mPhone='" + mPhone + '\'' +
+ ", mEmails=" + Arrays.toString(mEmails) +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final SimContact that = (SimContact) o;
+
+ if (mId != that.mId) return false;
+ if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
+ if (mPhone != null ? !mPhone.equals(that.mPhone) : that.mPhone != null) return false;
+ return Arrays.equals(mEmails, that.mEmails);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (mId ^ (mId >>> 32));
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (mPhone != null ? mPhone.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(mEmails);
+ return result;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mId);
+ dest.writeString(mName);
+ dest.writeString(mPhone);
+ dest.writeStringArray(mEmails);
+ }
+
+ /**
+ * Convert a collection of SIM contacts to a Cursor matching a query from
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_URI} with the provided projection.
+ *
+ * This allows a collection of SIM contacts to be displayed using the existing adapters for
+ * contacts.
+ */
+ public static final MatrixCursor convertToContactsCursor(Collection<SimContact> contacts,
+ String[] projection) {
+ final MatrixCursor result = new MatrixCursor(projection);
+ for (SimContact contact : contacts) {
+ contact.appendAsContactRow(result);
+ }
+ return result;
+ }
+
+ public static final Creator<SimContact> CREATOR = new Creator<SimContact>() {
+ @Override
+ public SimContact createFromParcel(Parcel source) {
+ long id = source.readLong();
+ String name = source.readString();
+ String phone = source.readString();
+ String[] emails = source.createStringArray();
+ return new SimContact(id, name, phone, emails);
+ }
+
+ @Override
+ public SimContact[] newArray(int size) {
+ return new SimContact[size];
+ }
+ };
+}
diff --git a/src/com/android/contacts/common/model/account/AccountWithDataSet.java b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
index 187f71c..37f6652 100644
--- a/src/com/android/contacts/common/model/account/AccountWithDataSet.java
+++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -20,13 +20,15 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
-import android.os.Parcelable;
import android.os.Parcel;
+import android.os.Parcelable;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.preference.ContactsPreferences;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
@@ -237,4 +239,25 @@
return ret;
}
+
+ public static AccountWithDataSet getDefaultOrBestFallback(ContactsPreferences preferences,
+ AccountTypeManager accountTypeManager) {
+ if (preferences.isDefaultAccountSet()) {
+ return preferences.getDefaultAccount();
+ }
+ List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(/* writableOnly */ true);
+
+ if (accounts.isEmpty()) {
+ return AccountWithDataSet.getNullAccount();
+ }
+
+ // Return the first google account
+ for (AccountWithDataSet account : accounts) {
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(account) && account.dataSet == null) {
+ return account;
+ }
+ }
+ // Arbitrarily return the first writable account
+ return accounts.get(0);
+ }
}
diff --git a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
index f544c7b..030575d 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
@@ -16,14 +16,22 @@
package com.android.contacts.common.preference;
-import android.app.ActionBar;
+import android.content.res.Configuration;
import android.database.Cursor;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.provider.ContactsContract.ProviderStatus;
-import android.provider.ContactsContract.QuickContact;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
+import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
import com.android.contacts.common.R;
import com.android.contacts.common.list.ProviderStatusWatcher;
@@ -44,13 +52,19 @@
private ProviderStatusWatcher mProviderStatusWatcher;
+ private AppCompatDelegate mCompatDelegate;
+
public static final String EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
@Override
protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ mCompatDelegate = AppCompatDelegate.create(this, null);
- final ActionBar actionBar = getActionBar();
+ super.onCreate(savedInstanceState);
+ mCompatDelegate.onCreate(savedInstanceState);
+
+
+ final ActionBar actionBar = mCompatDelegate.getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
}
@@ -80,6 +94,71 @@
}
}
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mCompatDelegate.onPostCreate(savedInstanceState);
+ }
+
+ public void setSupportActionBar(Toolbar toolbar) {
+ mCompatDelegate.setSupportActionBar(toolbar);
+ }
+
+ @NonNull
+ @Override
+ public MenuInflater getMenuInflater() {
+ return mCompatDelegate.getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutRes) {
+ mCompatDelegate.setContentView(layoutRes);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ mCompatDelegate.setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ mCompatDelegate.setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ mCompatDelegate.addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ mCompatDelegate.onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ mCompatDelegate.setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mCompatDelegate.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mCompatDelegate.onDestroy();
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ mCompatDelegate.invalidateOptionsMenu();
+ }
+
protected void showAboutFragment() {
getFragmentManager().beginTransaction()
.replace(android.R.id.content, AboutPreferenceFragment.newInstance(), TAG_ABOUT)
@@ -107,8 +186,8 @@
}
}
- private void setActivityTitle(int res) {
- final ActionBar actionBar = getActionBar();
+ private void setActivityTitle(@StringRes int res) {
+ final ActionBar actionBar = mCompatDelegate.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(res);
}
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
index f4187eb..1ae2b9d 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -206,6 +206,10 @@
mPreferences.edit().putString(mDefaultAccountKey, accountWithDataSet.stringify()).commit();
}
+ public boolean isDefaultAccountSet() {
+ return mDefaultAccount != null || mPreferences.contains(mDefaultAccountKey);
+ }
+
/**
* @return false if there is only one writable account or no requirement to return true is met.
* true if the contact editor should show the "accounts changed" notification, that is:
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index 89208a8..b3b2832 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -18,10 +18,12 @@
import android.app.Activity;
import android.app.LoaderManager;
+import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.Loader;
import android.content.res.Resources;
import android.database.Cursor;
@@ -32,15 +34,22 @@
import android.provider.BlockedNumberContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Profile;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.LocalBroadcastManager;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.R;
import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.R;
import com.android.contacts.common.compat.TelecomManagerUtil;
import com.android.contacts.common.compat.TelephonyManagerCompat;
-import com.android.contacts.common.interactions.ImportDialogFragment;
import com.android.contacts.common.interactions.ExportDialogFragment;
+import com.android.contacts.common.interactions.ImportDialogFragment;
import com.android.contacts.common.list.ContactListFilter;
import com.android.contacts.common.list.ContactListFilterController;
import com.android.contacts.common.logging.ScreenEvent.ScreenType;
@@ -122,6 +131,9 @@
private ProfileListener mListener;
+ private ViewGroup mRootView;
+ private SaveServiceResultListener mSaveServiceListener;
+
private final LoaderManager.LoaderCallbacks<Cursor> mProfileLoaderListener =
new LoaderManager.LoaderCallbacks<Cursor>() {
@@ -165,6 +177,25 @@
}
@Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // Wrap the preference view in a FrameLayout so we can show a snackbar
+ mRootView = new FrameLayout(getActivity());
+ final View list = super.onCreateView(inflater, mRootView, savedInstanceState);
+ mRootView.addView(list);
+ return mRootView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mSaveServiceListener = new SaveServiceResultListener();
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
+ mSaveServiceListener,
+ new IntentFilter(ContactSaveService.BROADCAST_SIM_IMPORT_COMPLETE));
+ }
+
+ @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -212,6 +243,13 @@
getLoaderManager().restartLoader(LOADER_PROFILE, null, mProfileLoaderListener);
}
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mSaveServiceListener);
+ mRootView = null;
+ }
+
public void updateMyInfoPreference(boolean hasProfile, String displayName, long contactId) {
final CharSequence summary = hasProfile ? displayName : getString(R.string.set_up_profile);
mMyInfoPreference.setSummary(summary);
@@ -361,5 +399,30 @@
}
}
}
+
+ private class SaveServiceResultListener extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final long now = System.currentTimeMillis();
+ final long opStart = intent.getLongExtra(
+ ContactSaveService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
+
+ // If it's been over 30 seconds the user is likely in a different context so suppress
+ // the toast message.
+ if (now - opStart > 30*1000) return;
+
+ final int code = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_CODE,
+ ContactSaveService.RESULT_UNKNOWN);
+ final int count = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_COUNT, -1);
+ if (code == ContactSaveService.RESULT_SUCCESS && count > 0) {
+ Snackbar.make(mRootView, getResources().getQuantityString(
+ R.plurals.sim_import_success_toast_fmt, count, count),
+ Snackbar.LENGTH_LONG).show();
+ } else if (code == ContactSaveService.RESULT_FAILURE) {
+ Snackbar.make(mRootView, R.string.sim_import_failed_toast,
+ Snackbar.LENGTH_LONG).show();
+ }
+ }
+ }
}
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
new file mode 100644
index 0000000..de2b014
--- /dev/null
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.editor;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListPopupWindow;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountDisplayInfo;
+import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountsListAdapter;
+import com.android.contacts.util.UiClosables;
+
+import java.util.List;
+
+/**
+ * Controls the display of an account selector or header.
+ *
+ * TODO: This was mostly copied from {@link RawContactEditorView}. The code in that class
+ * should probably be modified to use this instead of leaving it duplicated.
+ */
+public class AccountHeaderPresenter {
+
+ public interface Observer {
+ void onChange(AccountHeaderPresenter sender);
+
+ public static final Observer NONE = new Observer() {
+ @Override
+ public void onChange(AccountHeaderPresenter sender) {
+ }
+ };
+ }
+
+ private final Context mContext;
+ private AccountDisplayInfoFactory mAccountDisplayInfoFactory;
+
+ private AccountWithDataSet mCurrentAccount;
+
+ // Account header
+ private final View mAccountHeaderContainer;
+ private TextView mAccountHeaderType;
+ private TextView mAccountHeaderName;
+ private ImageView mAccountHeaderIcon;
+ private ImageView mAccountHeaderExpanderIcon;
+
+ // This would be different if the account was readonly
+ @StringRes
+ private int mSelectorTitle = R.string.editor_account_selector_title;
+
+ private Observer mObserver = Observer.NONE;
+
+ public AccountHeaderPresenter(View container) {
+ mContext = container.getContext();
+ mAccountHeaderContainer = container;
+ mAccountHeaderType = (TextView) container.findViewById(R.id.account_type);
+ mAccountHeaderName = (TextView) container.findViewById(R.id.account_name);
+ mAccountHeaderIcon = (ImageView) container.findViewById(R.id.account_type_icon);
+ mAccountHeaderExpanderIcon = (ImageView) container.findViewById(R.id.account_expander_icon);
+
+ mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(mContext);
+ }
+
+ public void setObserver(Observer observer) {
+ mObserver = observer;
+ }
+
+ public void setCurrentAccount(@NonNull AccountWithDataSet account) {
+ if (mCurrentAccount != null && mCurrentAccount.equals(account)) {
+ return;
+ }
+ mCurrentAccount = account;
+ if (mObserver != null) {
+ mObserver.onChange(this);
+ }
+ updateDisplayedAccount();
+ }
+
+ public AccountWithDataSet getCurrentAccount() {
+ return mCurrentAccount;
+ }
+
+ private void updateDisplayedAccount() {
+ mAccountHeaderContainer.setVisibility(View.GONE);
+ if (mCurrentAccount == null) return;
+
+ final AccountDisplayInfo account =
+ mAccountDisplayInfoFactory.getAccountDisplayInfo(mCurrentAccount);
+
+ final String accountLabel = getAccountLabel(account);
+
+ // Either the account header or selector should be shown, not both.
+ final List<AccountWithDataSet> accounts =
+ AccountTypeManager.getInstance(mContext).getAccounts(true);
+
+ if (accounts.size() > 1) {
+ addAccountSelector(accountLabel);
+ } else {
+ addAccountHeader(accountLabel);
+ }
+ }
+
+ private void addAccountHeader(String accountLabel) {
+ mAccountHeaderContainer.setVisibility(View.VISIBLE);
+
+ // Set the account name
+ mAccountHeaderName.setVisibility(View.VISIBLE);
+ mAccountHeaderName.setText(accountLabel);
+
+ // Set the account type
+ final String selectorTitle = mContext.getResources().getString(mSelectorTitle);
+ mAccountHeaderType.setText(selectorTitle);
+
+ // Set the icon
+ final AccountDisplayInfo displayInfo = mAccountDisplayInfoFactory
+ .getAccountDisplayInfo(mCurrentAccount);
+ mAccountHeaderIcon.setImageDrawable(displayInfo.getIcon());
+
+ // Set the content description
+ mAccountHeaderContainer.setContentDescription(
+ EditorUiUtils.getAccountInfoContentDescription(accountLabel,
+ selectorTitle));
+ }
+
+ private void addAccountSelector(CharSequence nameLabel) {
+ final View.OnClickListener onClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final ListPopupWindow popup = new ListPopupWindow(mContext, null);
+ final AccountsListAdapter adapter =
+ new AccountsListAdapter(mContext,
+ AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
+ mCurrentAccount);
+ popup.setWidth(mAccountHeaderContainer.getWidth());
+ popup.setAnchorView(mAccountHeaderContainer);
+ popup.setAdapter(adapter);
+ popup.setModal(true);
+ popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+ popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ UiClosables.closeQuietly(popup);
+ final AccountWithDataSet newAccount = adapter.getItem(position);
+ setCurrentAccount(newAccount);
+ }
+ });
+ popup.show();
+ }
+ };
+ setUpAccountSelector(nameLabel.toString(), onClickListener);
+ }
+
+ private void setUpAccountSelector(String nameLabel, View.OnClickListener listener) {
+ addAccountHeader(nameLabel);
+ // Add handlers for choosing another account to save to.
+ mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
+ mAccountHeaderContainer.setOnClickListener(listener);
+ }
+
+ private String getAccountLabel(AccountDisplayInfo account) {
+ // TODO: if used from editor this would need to be different if editing the user's profile.
+ return account.getNameLabel().toString();
+ }
+}
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 90aad4c..48e4bee 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -143,8 +143,6 @@
private static final String KEY_IS_EDIT = "isEdit";
private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
- private static final String KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY = "isReadOnly";
-
// Phone option menus
private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
@@ -350,7 +348,6 @@
protected RawContactDeltaList mState;
protected int mStatus;
protected long mRawContactIdToDisplayAlone = -1;
- protected boolean mRawContactDisplayAloneIsReadOnly = false;
// Whether to show the new contact blank form and if it's corresponding delta is ready.
protected boolean mHasNewContact;
@@ -497,8 +494,6 @@
// Read state from savedState. No loading involved here
mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
mStatus = savedState.getInt(KEY_STATUS);
- mRawContactDisplayAloneIsReadOnly = savedState.getBoolean(
- KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY);
mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
@@ -629,8 +624,6 @@
outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
outState.putBoolean(KEY_IS_EDIT, mIsEdit);
outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
- outState.putBoolean(KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY,
- mRawContactDisplayAloneIsReadOnly);
outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
@@ -780,7 +773,7 @@
}
// Save menu is invisible when there's only one read only contact in the editor.
- saveMenu.setVisible(!mRawContactDisplayAloneIsReadOnly);
+ saveMenu.setVisible(!isEditingReadOnlyRawContact());
if (saveMenu.isVisible()) {
// Since we're using a custom action layout we have to manually hook up the handler.
saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
@@ -1009,6 +1002,16 @@
}
/**
+ * @return true if the single raw contact we're looking at is read-only.
+ */
+ private boolean isEditingReadOnlyRawContact() {
+ return hasValidState() && mRawContactIdToDisplayAlone > 0
+ && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
+ .getAccountType(AccountTypeManager.getInstance(mContext))
+ .areContactsWritable();
+ }
+
+ /**
* Return true if there are any edits to the current contact which need to
* be saved.
*/
@@ -1860,13 +1863,6 @@
getEditorActivity().changePhoto(getPhotoMode());
}
- @Override
- public void onRawContactSelected(long rawContactId, boolean isReadOnly) {
- mRawContactDisplayAloneIsReadOnly = isReadOnly;
- mRawContactIdToDisplayAlone = rawContactId;
- bindEditors();
- }
-
private int getPhotoMode() {
return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
: PhotoActionPopup.Modes.NO_PHOTO;
diff --git a/src/com/android/contacts/editor/RawContactEditorView.java b/src/com/android/contacts/editor/RawContactEditorView.java
index bbd23d4..d3c7535 100644
--- a/src/com/android/contacts/editor/RawContactEditorView.java
+++ b/src/com/android/contacts/editor/RawContactEditorView.java
@@ -44,7 +44,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
-import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListPopupWindow;
@@ -62,8 +61,6 @@
import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.common.model.account.DeviceLocalAccountType;
-import com.android.contacts.common.model.account.SimAccountType;
import com.android.contacts.common.model.dataitem.CustomDataItem;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.util.AccountsListAdapter;
@@ -73,7 +70,6 @@
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -120,76 +116,7 @@
* Invoked after editors have been bound for the contact.
*/
public void onEditorsBound();
-
- /**
- * Invoked when a rawcontact from linked contacts is selected in editor.
- */
- void onRawContactSelected(long rawContactId, boolean isReadOnly);
}
-
- /**
- * Used to list the account info for the given raw contacts list.
- */
- private static final class RawContactAccountListAdapter extends BaseAdapter {
- private final LayoutInflater mInflater;
- private final Context mContext;
- private final RawContactDeltaList mRawContactDeltas;
-
- public RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas) {
- mContext = context;
- mRawContactDeltas = new RawContactDeltaList();
- for (RawContactDelta rawContactDelta : rawContactDeltas) {
- if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
- mRawContactDeltas.add(rawContactDelta);
- }
- }
- mInflater = LayoutInflater.from(context);
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- final View resultView = convertView != null ? convertView
- : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
-
- final RawContactDelta rawContactDelta = mRawContactDeltas.get(position);
-
- final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1);
- final AccountType accountType = rawContactDelta.getRawContactAccountType(mContext);
- text1.setText(accountType.getDisplayLabel(mContext));
-
- final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
- final String accountName = rawContactDelta.getAccountName();
- if (TextUtils.isEmpty(accountName) || accountType instanceof DeviceLocalAccountType
- || accountType instanceof SimAccountType) {
- text2.setVisibility(View.GONE);
- } else {
- // Truncate email addresses in the middle so we don't lose the domain
- text2.setText(accountName);
- text2.setEllipsize(TextUtils.TruncateAt.MIDDLE);
- }
-
- final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
- icon.setImageDrawable(accountType.getDisplayIcon(mContext));
-
- return resultView;
- }
-
- @Override
- public int getCount() {
- return mRawContactDeltas.size();
- }
-
- @Override
- public RawContactDelta getItem(int position) {
- return mRawContactDeltas.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return getItem(position).getRawContactId();
- }
- }
-
/**
* Sorts kinds roughly the same as quick contacts; we diverge in the following ways:
* <ol>
@@ -578,7 +505,6 @@
}
private void pickRawContactDelta() {
- // Build the kind section data list map
vlog("parse: " + mRawContactDeltas.size() + " rawContactDelta(s)");
for (int j = 0; j < mRawContactDeltas.size(); j++) {
final RawContactDelta rawContactDelta = mRawContactDeltas.get(j);
@@ -618,15 +544,17 @@
for (int i = 0; i < dataKindSize; i++) {
final DataKind dataKind = dataKinds.get(i);
- if (dataKind == null) {
- vlog("parse: " + i + " " + dataKind.mimeType + " dropped null data kind");
+ // Skip null and un-editable fields.
+ if (dataKind == null || !dataKind.editable) {
+ vlog("parse: " + i +
+ (dataKind == null ? " dropped null data kind"
+ : " dropped uneditable mimetype: " + dataKind.mimeType));
continue;
}
final String mimeType = dataKind.mimeType;
// Skip psuedo mime types
- if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
- || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
+ if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
continue;
}
@@ -777,51 +705,16 @@
? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
: account.getNameLabel().toString();
- // Either the account header or selector should be shown, not both.
+ addAccountHeader(accountLabel);
+
+ // If we're saving a new contact and there are multiple accounts, add the account selector.
final List<AccountWithDataSet> accounts =
AccountTypeManager.getInstance(getContext()).getAccounts(true);
-
- if (mHasNewContact && !mIsUserProfile) {
- if (accounts.size() > 1) {
- addAccountSelector(mCurrentRawContactDelta, accountLabel);
- } else {
- addAccountHeader(accountLabel);
- }
- } else {
- // The raw contact selector should only display linked raw contacts that can be edited
- // in the full editor (i.e. they are not newly created raw contacts)
- final RawContactAccountListAdapter adapter = new RawContactAccountListAdapter(
- getContext(), getRawContactDeltaListForSelector(mRawContactDeltas));
- if (adapter.getCount() > 0 && !mIsEditingReadOnlyRawContactWithNewContact) {
- addRawContactAccountSelector(accountLabel, adapter);
- } else {
- addAccountHeader(accountLabel);
- }
+ if (mHasNewContact && !mIsUserProfile && accounts.size() > 1) {
+ addAccountSelector(mCurrentRawContactDelta);
}
}
- private RawContactDeltaList getRawContactDeltaListForSelector(
- RawContactDeltaList rawContactDeltas) {
- // Sort raw contacts so google accounts come first
- Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext()));
-
- final RawContactDeltaList result = new RawContactDeltaList();
- for (int i = 0; i < rawContactDeltas.size(); i++) {
- final RawContactDelta rawContactDelta = rawContactDeltas.get(i);
- if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
- // Only add raw contacts that can be opened in the editor
- result.add(rawContactDelta);
- }
- }
- // Don't return a list of size 1 that would just open the current raw contact being edited.
- if (result.size() == 1 && result.get(0).getRawContactAccountType(
- getContext()).areContactsWritable()) {
- result.clear();
- return result;
- }
- return result;
- }
-
private void addAccountHeader(String accountLabel) {
mAccountHeaderContainer.setVisibility(View.VISIBLE);
@@ -846,8 +739,10 @@
selectorTitle));
}
- private void addAccountSelector(final RawContactDelta rawContactDelta, CharSequence nameLabel) {
- final View.OnClickListener onClickListener = new View.OnClickListener() {
+ private void addAccountSelector(final RawContactDelta rawContactDelta) {
+ // Add handlers for choosing another account to save to.
+ mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
+ mAccountHeaderContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
@@ -877,53 +772,7 @@
});
popup.show();
}
- };
- setUpAccountSelector(nameLabel.toString(), onClickListener);
- }
-
- private void addRawContactAccountSelector(String nameLabel,
- final RawContactAccountListAdapter adapter) {
- final View.OnClickListener onClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
- popup.setWidth(mAccountHeaderContainer.getWidth());
- popup.setAnchorView(mAccountHeaderContainer);
- popup.setAdapter(adapter);
- popup.setModal(true);
- popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
- popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position,
- long id) {
- UiClosables.closeQuietly(popup);
- final long rawContactId = adapter.getItemId(position);
- // Only switch if it's actually a different raw contact.
- if (rawContactId != mCurrentRawContactDelta.getRawContactId()
- && mListener != null) {
- final RawContactDelta rawContactDelta = adapter.getItem(position);
- final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
- getContext());
- final AccountType accountType = rawContactDelta.getAccountType(
- accountTypes);
- final boolean isReadOnly = !accountType.areContactsWritable();
- // Reset state.
- mIsExpanded = false;
- mListener.onRawContactSelected(rawContactId, isReadOnly);
- }
- }
- });
- popup.show();
- }
- };
- setUpAccountSelector(nameLabel, onClickListener);
- }
-
- private void setUpAccountSelector(String nameLabel, OnClickListener listener) {
- addAccountHeader(nameLabel);
- // Add handlers for choosing another account to save to.
- mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
- mAccountHeaderContainer.setOnClickListener(listener);
+ });
}
private void addPhotoView() {
diff --git a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
index 8364b7b..c9ea3b6 100644
--- a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
+++ b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
@@ -6,6 +6,7 @@
import android.content.pm.PackageManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
+import android.support.test.filters.Suppress;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
@@ -36,6 +37,8 @@
* -e class com.android.contacts.NoPermissionsLaunchSmokeTest
*/
@MediumTest
+// suppressed because failed assumptions are reported as test failures by the build server
+@Suppress
@RunWith(AndroidJUnit4.class)
public class NoPermissionsLaunchSmokeTest {
private static final long TIMEOUT = 5000;
diff --git a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
new file mode 100644
index 0000000..5b25eb2
--- /dev/null
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.database;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.DatabaseUtils;
+import android.provider.ContactsContract;
+import android.support.annotation.RequiresApi;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.tests.AccountsTestHelper;
+import com.android.contacts.tests.SimContactsTestHelper;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static android.os.Build.VERSION_CODES;
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertThat;
+
+import android.support.test.filters.Suppress;
+
+@RunWith(Enclosed.class)
+public class SimContactDaoTests {
+
+ // Lollipop MR1 required for AccountManager.removeAccountExplicitly
+ @RequiresApi(api = VERSION_CODES.LOLLIPOP_MR1)
+ @SdkSuppress(minSdkVersion = VERSION_CODES.LOLLIPOP_MR1)
+ @LargeTest
+ @RunWith(AndroidJUnit4.class)
+ public static class ImportIntegrationTest {
+ private AccountWithDataSet mAccount;
+ private AccountsTestHelper mAccountsHelper;
+ private ContentResolver mResolver;
+
+ @Before
+ public void setUp() throws Exception {
+ mAccountsHelper = new AccountsTestHelper();
+ mAccount = mAccountsHelper.addTestAccount();
+ mResolver = getContext().getContentResolver();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mAccountsHelper.cleanup();
+ }
+
+ @Test
+ public void importFromSim() throws Exception {
+ final SimContactDao sut = new SimContactDao(getContext());
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(1, "Test One", "15095550101", null),
+ new SimContact(2, "Test Two", "15095550102", null),
+ new SimContact(3, "Test Three", "15095550103", new String[] {
+ "user@example.com", "user2@example.com"
+ })
+ ), mAccount);
+
+ Cursor cursor = queryContactWithName("Test One");
+ assertThat(cursor, hasCount(2));
+ assertThat(cursor, hasName("Test One"));
+ assertThat(cursor, hasPhone("15095550101"));
+ cursor.close();
+
+ cursor = queryContactWithName("Test Two");
+ assertThat(cursor, hasCount(2));
+ assertThat(cursor, hasName("Test Two"));
+ assertThat(cursor, hasPhone("15095550102"));
+ cursor.close();
+
+ cursor = queryContactWithName("Test Three");
+ assertThat(cursor, hasCount(4));
+ assertThat(cursor, hasName("Test Three"));
+ assertThat(cursor, hasPhone("15095550103"));
+ assertThat(cursor, allOf(hasEmail("user@example.com"), hasEmail("user2@example.com")));
+ cursor.close();
+ }
+
+ @Test
+ public void importContactWhichOnlyHasName() throws Exception {
+ final SimContactDao sut = new SimContactDao(getContext());
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(1, "Test importJustName", null, null)
+ ), mAccount);
+
+ Cursor cursor = queryAllDataInAccount();
+
+ assertThat(cursor, hasCount(1));
+ assertThat(cursor, hasName("Test importJustName"));
+ cursor.close();
+ }
+
+ @Test
+ public void importContactWhichOnlyHasPhone() throws Exception {
+ final SimContactDao sut = new SimContactDao(getContext());
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(1, null, "15095550111", null)
+ ), mAccount);
+
+ Cursor cursor = queryAllDataInAccount();
+
+ assertThat(cursor, hasCount(1));
+ assertThat(cursor, hasPhone("15095550111"));
+ cursor.close();
+ }
+
+ @Test
+ public void ignoresEmptyContacts() throws Exception {
+ final SimContactDao sut = new SimContactDao(getContext());
+
+ // This probably isn't possible but we'll test it to demonstrate expected behavior and
+ // just in case it does occur
+ sut.importContacts(Arrays.asList(
+ new SimContact(1, null, null, null),
+ new SimContact(2, null, null, null),
+ new SimContact(3, null, null, null),
+ new SimContact(4, "Not null", null, null)
+ ), mAccount);
+
+ final Cursor contactsCursor = queryAllRawContactsInAccount();
+ assertThat(contactsCursor, hasCount(1));
+ contactsCursor.close();
+
+ final Cursor dataCursor = queryAllDataInAccount();
+ assertThat(dataCursor, hasCount(1));
+
+ dataCursor.close();
+ }
+
+ private Cursor queryAllRawContactsInAccount() {
+ return new StringableCursor(mResolver.query(ContactsContract.RawContacts.CONTENT_URI, null,
+ ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
+ ContactsContract.RawContacts.ACCOUNT_TYPE+ "=?",
+ new String[] {
+ mAccount.name,
+ mAccount.type
+ }, null));
+ }
+
+ private Cursor queryAllDataInAccount() {
+ return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+ ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
+ ContactsContract.RawContacts.ACCOUNT_TYPE+ "=?",
+ new String[] {
+ mAccount.name,
+ mAccount.type
+ }, null));
+ }
+
+ private Cursor queryContactWithName(String name) {
+ return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+ ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
+ ContactsContract.RawContacts.ACCOUNT_TYPE+ "=? AND " +
+ ContactsContract.Data.DISPLAY_NAME + "=?",
+ new String[] {
+ mAccount.name,
+ mAccount.type,
+ name
+ }, null));
+ }
+ }
+
+ @LargeTest
+ // suppressed because failed assumptions are reported as test failures by the build server
+ @Suppress
+ @RunWith(AndroidJUnit4.class)
+ public static class ReadIntegrationTest {
+ private SimContactsTestHelper mSimTestHelper;
+ private ArrayList<ContentProviderOperation> mSimSnapshot;
+
+ @Before
+ public void setUp() throws Exception {
+ mSimTestHelper = new SimContactsTestHelper();
+
+ mSimTestHelper.assumeSimWritable();
+ if (!mSimTestHelper.isSimWritable()) return;
+
+ mSimSnapshot = mSimTestHelper.captureRestoreSnapshot();
+ mSimTestHelper.deleteAllSimContacts();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSimTestHelper.restore(mSimSnapshot);
+ }
+
+ @Test
+ public void readFromSim() {
+ mSimTestHelper.addSimContact("Test Simone", "15095550101");
+ mSimTestHelper.addSimContact("Test Simtwo", "15095550102");
+ mSimTestHelper.addSimContact("Test Simthree", "15095550103");
+
+ final SimContactDao sut = new SimContactDao(getContext());
+ final ArrayList<SimContact> contacts = sut.loadSimContacts();
+
+ assertThat(contacts.get(0), isSimContactWithNameAndPhone("Test Simone", "15095550101"));
+ assertThat(contacts.get(1), isSimContactWithNameAndPhone("Test Simtwo", "15095550102"));
+ assertThat(contacts.get(2), isSimContactWithNameAndPhone("Test Simthree", "15095550103"));
+ }
+ }
+
+ private static Matcher<SimContact> isSimContactWithNameAndPhone(final String name,
+ final String phone) {
+ return new BaseMatcher<SimContact>() {
+ @Override
+ public boolean matches(Object o) {
+ if (!(o instanceof SimContact)) return false;
+
+ SimContact other = (SimContact) o;
+
+ return name.equals(other.getName())
+ && phone.equals(other.getPhone());
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("SimContact with name=" + name + " and phone=" +
+ phone);
+ }
+ };
+ }
+
+ private static Matcher<Cursor> hasCount(final int count) {
+ return new BaseMatcher<Cursor>() {
+ @Override
+ public boolean matches(Object o) {
+ if (!(o instanceof Cursor)) return false;
+ return ((Cursor)o).getCount() == count;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Cursor with " + count + " rows");
+ }
+ };
+ }
+
+ private static Matcher<Cursor> hasMimeType(String type) {
+ return hasValueForColumn(ContactsContract.Data.MIMETYPE, type);
+ }
+
+ private static Matcher<Cursor> hasValueForColumn(final String column, final String value) {
+ return new BaseMatcher<Cursor>() {
+
+ @Override
+ public boolean matches(Object o) {
+ if (!(o instanceof Cursor)) return false;
+ final Cursor cursor = (Cursor)o;
+
+ final int index = cursor.getColumnIndexOrThrow(column);
+ return value.equals(cursor.getString(index));
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Cursor with " + column + "=" + value);
+ }
+ };
+ }
+
+ private static Matcher<Cursor> hasRowMatching(final Matcher<Cursor> rowMatcher) {
+ return new BaseMatcher<Cursor>() {
+ @Override
+ public boolean matches(Object o) {
+ if (!(o instanceof Cursor)) return false;
+ final Cursor cursor = (Cursor)o;
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ if (rowMatcher.matches(cursor)) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Cursor with row matching ");
+ rowMatcher.describeTo(description);
+ }
+ };
+ }
+
+ private static Matcher<Cursor> hasName(final String name) {
+ return hasRowMatching(allOf(
+ hasMimeType(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE),
+ hasValueForColumn(
+ ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)));
+ }
+
+ private static Matcher<Cursor> hasPhone(final String phone) {
+ return hasRowMatching(allOf(
+ hasMimeType(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE),
+ hasValueForColumn(
+ ContactsContract.CommonDataKinds.Phone.NUMBER, phone)));
+ }
+
+ private static Matcher<Cursor> hasEmail(final String email) {
+ return hasRowMatching(allOf(
+ hasMimeType(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE),
+ hasValueForColumn(
+ ContactsContract.CommonDataKinds.Email.ADDRESS, email)));
+ }
+
+ static class StringableCursor extends CursorWrapper {
+ public StringableCursor(Cursor cursor) {
+ super(cursor);
+ }
+
+ @Override
+ public String toString() {
+ final Cursor wrapped = getWrappedCursor();
+
+ if (wrapped.getCount() == 0) {
+ return "Empty Cursor";
+ }
+
+ return DatabaseUtils.dumpCursorToString(wrapped);
+ }
+ }
+
+ static Context getContext() {
+ return InstrumentationRegistry.getTargetContext();
+ }
+}
diff --git a/tests/src/com/android/contacts/common/model/SimContactTests.java b/tests/src/com/android/contacts/common/model/SimContactTests.java
new file mode 100644
index 0000000..de9ab5a
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/SimContactTests.java
@@ -0,0 +1,43 @@
+package com.android.contacts.common.model;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Created by mhagerott on 10/6/16.
+ */
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SimContactTests {
+ @Test
+ public void parcelRoundtrip() {
+ assertParcelsCorrectly(new SimContact(1, "name1", "phone1",
+ new String[] { "email1a", "email1b" }));
+ assertParcelsCorrectly(new SimContact(2, "name2", "phone2", null));
+ assertParcelsCorrectly(new SimContact(3, "name3", null,
+ new String[] { "email3" }));
+ assertParcelsCorrectly(new SimContact(4, null, "phone4",
+ new String[] { "email4" }));
+ assertParcelsCorrectly(new SimContact(5, null, null, null));
+ assertParcelsCorrectly(new SimContact(6, "name6", "phone6",
+ new String[0]));
+ }
+
+ private void assertParcelsCorrectly(SimContact contact) {
+ final Parcel parcel = Parcel.obtain();
+ parcel.writeParcelable(contact, 0);
+ parcel.setDataPosition(0);
+ final SimContact unparceled = parcel.readParcelable(
+ SimContact.class.getClassLoader());
+ assertThat(unparceled, equalTo(contact));
+ parcel.recycle();
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/AccountsTestHelper.java b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
new file mode 100644
index 0000000..be826f7
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.tests;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Build;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+@SuppressWarnings("MissingPermission")
+public class AccountsTestHelper {
+ public static final String TEST_ACCOUNT_TYPE = "com.android.contacts.tests.testauth.basic";
+
+ private final Context mContext;
+ private final AccountManager mAccountManager;
+ private final ContentResolver mResolver;
+
+ private Account mTestAccount;
+
+ public AccountsTestHelper() {
+ // Use context instead of target context because the test package has the permissions needed
+ // to add and remove accounts.
+ this(InstrumentationRegistry.getContext());
+ }
+
+ public AccountsTestHelper(Context context) {
+ mContext = context;
+ mAccountManager = AccountManager.get(mContext);
+ mResolver = mContext.getContentResolver();
+ }
+
+ public AccountWithDataSet addTestAccount() {
+ return addTestAccount(generateAccountName());
+ }
+
+ public String generateAccountName(String prefix) {
+ return prefix + "_t" + System.nanoTime();
+ }
+
+ public String generateAccountName() {
+ return generateAccountName("test");
+ }
+
+ public AccountWithDataSet addTestAccount(@NonNull String name) {
+ // remember the most recent one. If the caller wants to add multiple accounts they will
+ // have to keep track of them themselves.
+ mTestAccount = new Account(name, TEST_ACCOUNT_TYPE);
+ assertTrue(mAccountManager.addAccountExplicitly(mTestAccount, null, null));
+ return convertTestAccount();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
+ public void removeTestAccount(AccountWithDataSet account) {
+ final Account remove = account.getAccountOrNull();
+ mAccountManager.removeAccountExplicitly(remove);
+ }
+
+ public void removeContactsForAccount() {
+ // Not sure if this is necessary or if contacts are automatically cleaned up when the
+ // account is removed.
+ mResolver.delete(RawContacts.CONTENT_URI,
+ RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
+ new String[] { mTestAccount.name, mTestAccount.type });
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
+ public void cleanup() {
+ assertNotNull(mTestAccount);
+
+ // Note that we don't need to cleanup up the contact data associated with the account.
+ // CP2 will eventually do that automatically so as long as we're using unique account
+ // names we should be safe. Note that cleanup is not done synchronously when the account
+ // is removed so if multiple tests are using the same account name then the data should
+ // be manually deleted after each test run.
+
+ mAccountManager.removeAccountExplicitly(mTestAccount);
+ mTestAccount = null;
+ }
+
+ private AccountWithDataSet convertTestAccount() {
+ return new AccountWithDataSet(mTestAccount.name, mTestAccount.type, null);
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/SimContactsTestHelper.java b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
new file mode 100644
index 0000000..45ac8d9
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.tests;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
+import android.telephony.TelephonyManager;
+
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assume.assumeThat;
+import static org.junit.Assume.assumeTrue;
+
+public class SimContactsTestHelper {
+
+ private final Context mContext;
+ private final TelephonyManager mTelephonyManager;
+ private final ContentResolver mResolver;
+ private final SimContactDao mSimDao;
+
+ public SimContactsTestHelper() {
+ this(InstrumentationRegistry.getTargetContext());
+ }
+
+ public SimContactsTestHelper(Context context) {
+ mContext = context;
+ mResolver = context.getContentResolver();
+ mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ mSimDao = new SimContactDao(context);
+ }
+
+ public int getSimContactCount() {
+ Cursor cursor = mContext.getContentResolver().query(SimContactDao.ICC_CONTENT_URI,
+ null, null, null, null);
+ try {
+ return cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public ContentValues iccRow(long id, String name, String number, String emails) {
+ ContentValues values = new ContentValues();
+ values.put(SimContactDao._ID, id);
+ values.put(SimContactDao.NAME, name);
+ values.put(SimContactDao.NUMBER, number);
+ values.put(SimContactDao.EMAILS, emails);
+ return values;
+ }
+
+ public ContentProvider iccProviderExpectingNoQueries() {
+ return new MockContentProvider();
+ }
+
+ public ContentProvider emptyIccProvider() {
+ final MockContentProvider provider = new MockContentProvider();
+ provider.expectQuery(SimContactDao.ICC_CONTENT_URI)
+ .withDefaultProjection(
+ SimContactDao._ID, SimContactDao.NAME,
+ SimContactDao.NUMBER, SimContactDao.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnEmptyCursor();
+ return provider;
+ }
+
+ public Uri addSimContact(String name, String number) {
+ ContentValues values = new ContentValues();
+ // Oddly even though it's called name when querying we have to use "tag" for it to work
+ // when inserting.
+ if (name != null) {
+ values.put("tag", name);
+ }
+ if (number != null) {
+ values.put(SimContactDao.NUMBER, number);
+ }
+ return mResolver.insert(SimContactDao.ICC_CONTENT_URI, values);
+ }
+
+ public ContentProviderResult[] deleteAllSimContacts()
+ throws RemoteException, OperationApplicationException {
+ SimContactDao dao = new SimContactDao(mContext);
+ List<SimContact> contacts = dao.loadSimContacts();
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (SimContact contact : contacts) {
+ ops.add(ContentProviderOperation
+ .newDelete(SimContactDao.ICC_CONTENT_URI)
+ .withSelection(getWriteSelection(contact), null)
+ .build());
+ }
+ return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), ops);
+ }
+
+ public ContentProviderResult[] restore(ArrayList<ContentProviderOperation> restoreOps)
+ throws RemoteException, OperationApplicationException {
+ if (restoreOps == null) return null;
+
+ // Remove SIM contacts because we assume that caller wants the data to be in the exact
+ // state as when the restore ops were captured.
+ deleteAllSimContacts();
+ return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), restoreOps);
+ }
+
+ public ArrayList<ContentProviderOperation> captureRestoreSnapshot() {
+ ArrayList<SimContact> contacts = mSimDao.loadSimContacts();
+
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (SimContact contact : contacts) {
+ final String[] emails = contact.getEmails();
+ if (emails != null && emails.length > 0) {
+ throw new IllegalStateException("Cannot restore emails." +
+ " Please manually remove SIM contacts with emails.");
+ }
+ ops.add(ContentProviderOperation
+ .newInsert(SimContactDao.ICC_CONTENT_URI)
+ .withValue("tag", contact.getName())
+ .withValue("number", contact.getPhone())
+ .build());
+ }
+ return ops;
+ }
+
+ public String getWriteSelection(SimContact simContact) {
+ return "tag='" + simContact.getName() + "' AND " + SimContactDao.NUMBER + "='" +
+ simContact.getPhone() + "'";
+ }
+
+ public int deleteSimContact(@NonNull String name, @NonNull String number) {
+ // IccProvider doesn't use the selection args.
+ final String selection = "tag='" + name + "' AND " +
+ SimContactDao.NUMBER + "='" + number + "'";
+ return mResolver.delete(SimContactDao.ICC_CONTENT_URI, selection, null);
+ }
+
+ public boolean isSimReady() {
+ return mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY;
+ }
+
+ public boolean doesSimHaveContacts() {
+ return isSimReady() && getSimContactCount() > 0;
+ }
+
+ public boolean isSimWritable() {
+ if (!isSimReady()) return false;
+ final String name = "writabeProbe" + System.nanoTime();
+ final Uri uri = addSimContact(name, "15095550101");
+ return uri != null && deleteSimContact(name, "15095550101") == 1;
+ }
+
+ public void assumeSimReady() {
+ assumeTrue(isSimReady());
+ }
+
+ public void assumeHasSimContacts() {
+ assumeTrue(doesSimHaveContacts());
+ }
+
+ public void assumeSimCardAbsent() {
+ assumeThat(mTelephonyManager.getSimState(), equalTo(TelephonyManager.SIM_STATE_ABSENT));
+ }
+
+ // The emulator reports SIM_STATE_READY but writes are ignored. This verifies that the
+ // device will actually persist writes to the SIM card.
+ public void assumeSimWritable() {
+ assumeSimReady();
+ assumeTrue(isSimWritable());
+ }
+}