summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--proguard.flags2
-rw-r--r--res/anim/slide_and_fade_out.xml26
-rw-r--r--res/layout/fragment_sim_import.xml80
-rw-r--r--res/values/strings.xml22
-rw-r--r--res/values/styles.xml16
-rwxr-xr-xsrc/com/android/contacts/ContactSaveService.java56
-rw-r--r--src/com/android/contacts/SimImportFragment.java323
-rw-r--r--src/com/android/contacts/activities/PeopleActivity.java13
-rw-r--r--src/com/android/contacts/common/database/SimContactDao.java119
-rw-r--r--src/com/android/contacts/common/interactions/ImportDialogFragment.java12
-rw-r--r--src/com/android/contacts/common/list/ContactListAdapter.java2
-rw-r--r--src/com/android/contacts/common/model/SimContact.java200
-rw-r--r--src/com/android/contacts/common/model/account/AccountWithDataSet.java25
-rw-r--r--src/com/android/contacts/common/preference/ContactsPreferenceActivity.java89
-rw-r--r--src/com/android/contacts/common/preference/ContactsPreferences.java4
-rw-r--r--src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java67
-rw-r--r--src/com/android/contacts/editor/AccountHeaderPresenter.java185
-rw-r--r--tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java3
-rw-r--r--tests/src/com/android/contacts/common/database/SimContactDaoTests.java359
-rw-r--r--tests/src/com/android/contacts/common/model/SimContactTests.java43
-rw-r--r--tests/src/com/android/contacts/tests/AccountsTestHelper.java106
-rw-r--r--tests/src/com/android/contacts/tests/SimContactsTestHelper.java198
22 files changed, 1925 insertions, 25 deletions
diff --git a/proguard.flags b/proguard.flags
index 80ffe0ac3..e50e64095 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 000000000..7bb65d7dc
--- /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/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
new file mode 100644
index 000000000..9e2f49376
--- /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/values/strings.xml b/res/values/strings.xml
index 454777e02..5ec6f1c2b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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 d51392abf..fc52bc57a 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 @@ background and text color. See also android:style/Widget.Holo.TextView.ListSepar
<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/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index d4da58821..1ed36b54f 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 android.widget.Toast;
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.HashSet;
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 class ContactSaveService extends IntentService {
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 class ContactSaveService extends IntentService {
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 @@ public class ContactSaveService extends IntentService {
private Handler mMainHandler;
private GroupsDao mGroupsDao;
+ private SimContactDao mSimContactDao;
public ContactSaveService() {
super(TAG);
@@ -187,6 +202,7 @@ public class ContactSaveService extends IntentService {
public void onCreate() {
super.onCreate();
mGroupsDao = new GroupsDaoImpl(this);
+ mSimContactDao = new SimContactDao(this);
}
public static void registerListener(Listener listener) {
@@ -305,6 +321,8 @@ public class ContactSaveService extends IntentService {
setRingtone(intent);
} else if (ACTION_UNDO.equals(action)) {
undo(intent);
+ } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
+ importFromSim(intent);
}
}
@@ -812,7 +830,7 @@ public class ContactSaveService extends IntentService {
}
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 @@ public class ContactSaveService extends IntentService {
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/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
new file mode 100644
index 000000000..a9dcf9774
--- /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/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index b2a0a07b1..935c66fb7 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -20,15 +20,13 @@ import android.accounts.Account;
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.ContextCompat;
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.list.ProviderStatusWatcher.ProviderStatusList
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;
@@ -469,7 +464,7 @@ public class PeopleActivity extends ContactsDrawerActivity {
mSaveServiceListener = new SaveServiceListener();
LocalBroadcastManager.getInstance(this).registerReceiver(mSaveServiceListener,
- new IntentFilter(ContactSaveService.BROADCAST_ACTION_GROUP_DELETED));
+ new IntentFilter(ContactSaveService.BROADCAST_GROUP_DELETED));
}
@Override
@@ -731,7 +726,7 @@ public class PeopleActivity extends ContactsDrawerActivity {
@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;
}
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 000000000..f9fb4e817
--- /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 0c0ce73e5..991a9c2fd 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.view.ViewGroup;
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 class ImportDialogFragment extends DialogFragment
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 @@ public class ImportDialogFragment extends DialogFragment
.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 677aa46fe..6294f4788 100644
--- a/src/com/android/contacts/common/list/ContactListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -41,7 +41,7 @@ import java.util.Set;
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 000000000..a13ef67da
--- /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 187f71c0c..37f6652d1 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.accounts.Account;
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 @@ public class AccountWithDataSet implements Parcelable {
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 f544c7bdf..030575da1 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 @@ public final class ContactsPreferenceActivity extends PreferenceActivity impleme
private ProviderStatusWatcher mProviderStatusWatcher;
+ private AppCompatDelegate mCompatDelegate;
+
public static final String EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
@Override
protected void onCreate(Bundle savedInstanceState) {
+ mCompatDelegate = AppCompatDelegate.create(this, null);
+
super.onCreate(savedInstanceState);
+ mCompatDelegate.onCreate(savedInstanceState);
+
- final ActionBar actionBar = getActionBar();
+ final ActionBar actionBar = mCompatDelegate.getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
}
@@ -80,6 +94,71 @@ public final class ContactsPreferenceActivity extends PreferenceActivity impleme
}
}
+ @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 @@ public final class ContactsPreferenceActivity extends PreferenceActivity impleme
}
}
- 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 f4187ebd6..1ae2b9de0 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -206,6 +206,10 @@ public class ContactsPreferences implements OnSharedPreferenceChangeListener {
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 89208a8f3..b3b28322b 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -18,10 +18,12 @@ package com.android.contacts.common.preference;
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.preference.PreferenceFragment;
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 @@ public class DisplayOptionsPreferenceFragment extends PreferenceFragment
private ProfileListener mListener;
+ private ViewGroup mRootView;
+ private SaveServiceResultListener mSaveServiceListener;
+
private final LoaderManager.LoaderCallbacks<Cursor> mProfileLoaderListener =
new LoaderManager.LoaderCallbacks<Cursor>() {
@@ -165,6 +177,25 @@ public class DisplayOptionsPreferenceFragment extends PreferenceFragment
}
@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 @@ public class DisplayOptionsPreferenceFragment extends PreferenceFragment
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 @@ public class DisplayOptionsPreferenceFragment extends PreferenceFragment
}
}
}
+
+ 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 000000000..de2b0143b
--- /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/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
index 8364b7b07..c9ea3b6aa 100644
--- a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
+++ b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
@@ -6,6 +6,7 @@ import android.content.Intent;
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 @@ import static org.junit.Assume.assumeTrue;
* -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 000000000..5b25eb2b5
--- /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 000000000..de9ab5a1f
--- /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 000000000..be826f7ec
--- /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 000000000..45ac8d96f
--- /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());
+ }
+}