Include item disabled state for talkback in SIM import

Test
manually verified that SIM import list functions the same as previously
and that disabled state is now included in talkback description for
items

Bug 32950353

Change-Id: Iff93cde51b0d5d0edfb1096e5bfd539c68de941a
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
index d7a83f5..6688898 100644
--- a/res/layout/fragment_sim_import.xml
+++ b/res/layout/fragment_sim_import.xml
@@ -56,8 +56,8 @@
             android:background="?android:colorBackground"
             android:minHeight="48dp"
             android:orientation="horizontal"
-            android:paddingEnd="16dp"
-            android:paddingStart="16dp">
+            android:paddingEnd="@dimen/activity_horizontal_margin"
+            android:paddingStart="@dimen/activity_horizontal_margin">
 
             <ImageView
                 android:id="@+id/account_type_icon"
@@ -84,7 +84,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_vertical"
-                android:layout_marginEnd="10dp"
+                android:layout_marginEnd="5dp"
                 android:background="@null"
                 android:clickable="true"
                 android:contentDescription="@string/show_more_content_description"
diff --git a/res/layout/nav_header_main.xml b/res/layout/nav_header_main.xml
index 37a33f4..518e6e6 100644
--- a/res/layout/nav_header_main.xml
+++ b/res/layout/nav_header_main.xml
@@ -23,9 +23,9 @@
     android:background="@color/contacts_accent_color"
     android:gravity="bottom"
     android:orientation="vertical"
-    android:paddingBottom="@dimen/nav_activity_vertical_margin"
-    android:paddingLeft="@dimen/nav_activity_horizontal_margin"
-    android:paddingRight="@dimen/nav_activity_horizontal_margin"
-    android:paddingTop="@dimen/nav_activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
     android:theme="@style/ThemeOverlay.AppCompat.Dark">
 </LinearLayout>
diff --git a/res/layout/sim_import_list_item.xml b/res/layout/sim_import_list_item.xml
new file mode 100644
index 0000000..86f427e
--- /dev/null
+++ b/res/layout/sim_import_list_item.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<CheckedTextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/text"
+    android:layout_width="match_parent"
+    android:layout_height="?android:listPreferredItemHeight"
+    android:checkMark="?android:listChoiceIndicatorMultiple"
+    android:duplicateParentState="true"
+    android:gravity="center_vertical"
+    android:paddingEnd="@dimen/activity_horizontal_margin"
+    android:paddingStart="@dimen/activity_horizontal_margin"
+    android:textAppearance="?android:textAppearanceListItem"/>
diff --git a/res/layout/sim_import_list_item_disabled.xml b/res/layout/sim_import_list_item_disabled.xml
new file mode 100644
index 0000000..4647049
--- /dev/null
+++ b/res/layout/sim_import_list_item_disabled.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/text"
+    style="?android:checkedTextViewStyle"
+    android:layout_width="match_parent"
+    android:layout_height="?android:listPreferredItemHeight"
+    android:drawableEnd="?android:listChoiceIndicatorMultiple"
+    android:enabled="false"
+    android:gravity="center_vertical"
+    android:orientation="vertical"
+    android:paddingEnd="@dimen/activity_horizontal_margin"
+    android:paddingStart="@dimen/activity_horizontal_margin"
+    android:textAppearance="?android:textAppearanceListItem"
+    android:textColor="@color/contact_list_name_text_color"/>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index de9764d..d434ef2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -244,9 +244,9 @@
     <!-- Navigation drawer header height, the same as the status bar in landscape and portrait modes -->
     <dimen name="nav_header_height">24dp</dimen>
 
-    <!-- Navigation drawer margins, per the Android Design guidelines. -->
-    <dimen name="nav_activity_horizontal_margin">16dp</dimen>
-    <dimen name="nav_activity_vertical_margin">16dp</dimen>
+    <!-- Default activity margins, per the Android Material Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
 
     <dimen name="nav_new_badge_corners">3dp</dimen>
     <dimen name="nav_new_badge_vertical_pad">2dp</dimen>
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index a873842..5f5de3e 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -20,43 +20,40 @@
 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.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.widget.Snackbar;
 import android.support.v4.util.ArrayMap;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.ContentLoadingProgressBar;
 import android.support.v7.widget.Toolbar;
+import android.util.SparseBooleanArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
 import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
 import android.widget.ListView;
+import android.widget.TextView;
 
-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.SimCard;
 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 com.google.common.primitives.Longs;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 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
@@ -64,10 +61,11 @@
  */
 public class SimImportFragment extends DialogFragment
         implements LoaderManager.LoaderCallbacks<SimImportFragment.LoaderResult>,
-        MultiSelectEntryContactListAdapter.SelectedContactsListener, AbsListView.OnScrollListener {
+        AdapterView.OnItemClickListener, AbsListView.OnScrollListener {
 
     private static final String KEY_SUFFIX_SELECTED_IDS = "_selectedIds";
     private static final String ARG_SUBSCRIPTION_ID = "subscriptionId";
+    private static final String TAG = "SimImportFragment";
 
     private ContactsPreferences mPreferences;
     private AccountTypeManager mAccountTypeManager;
@@ -80,6 +78,8 @@
     private ListView mListView;
     private View mImportButton;
 
+    private final Map<AccountWithDataSet, long[]> mPerAccountCheckedIds = new ArrayMap<>();
+
     private int mSubscriptionId;
 
     @Override
@@ -91,12 +91,6 @@
         mAccountTypeManager = AccountTypeManager.getInstance(getActivity());
         mAdapter = new SimContactAdapter(getActivity());
 
-        // This needs to be set even though photos aren't loaded because the adapter assumes it
-        // will be non-null
-        mAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity()));
-        mAdapter.setDisplayCheckBoxes(true);
-        mAdapter.setHasHeader(0, false);
-
         final Bundle args = getArguments();
         mSubscriptionId = args == null ? SimCard.NO_SUBSCRIPTION_ID :
                 args.getInt(ARG_SUBSCRIPTION_ID, SimCard.NO_SUBSCRIPTION_ID);
@@ -137,7 +131,10 @@
         mAccountHeaderPresenter.setObserver(new AccountHeaderPresenter.Observer() {
             @Override
             public void onChange(AccountHeaderPresenter sender) {
+                rememberSelectionsForCurrentAccount();
                 mAdapter.setAccount(sender.getCurrentAccount());
+                showSelectionsForCurrentAccount();
+                updateToolbarWithCurrentSelections();
             }
         });
         mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount());
@@ -146,17 +143,8 @@
         mListView = (ListView) view.findViewById(R.id.list);
         mListView.setOnScrollListener(this);
         mListView.setAdapter(mAdapter);
-        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                if (mAdapter.existsInCurrentAccount(position)) {
-                    Snackbar.make(getView(), R.string.sim_import_contact_exists_toast,
-                            Snackbar.LENGTH_LONG).show();
-                } else {
-                    mAdapter.toggleSelectionOfContactId(id);
-                }
-            }
-        });
+        mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+        mListView.setOnItemClickListener(this);
         mImportButton = view.findViewById(R.id.import_button);
         mImportButton.setOnClickListener(new View.OnClickListener() {
             @Override
@@ -166,8 +154,6 @@
                 dismiss();
             }
         });
-        mImportButton.setVisibility(mAdapter.getSelectedContactIds().size() > 0 ?
-                View.VISIBLE : View.GONE);
 
         mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
         mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@@ -178,11 +164,55 @@
         });
 
         mLoadingIndicator = (ContentLoadingProgressBar) view.findViewById(R.id.loading_progress);
-        mAdapter.setSelectedContactsListener(this);
 
         return view;
     }
 
+    private void rememberSelectionsForCurrentAccount() {
+        final AccountWithDataSet current = mAdapter.getAccount();
+        final long[] ids = mListView.getCheckedItemIds();
+        Arrays.sort(ids);
+        mPerAccountCheckedIds.put(current, ids);
+    }
+
+    private void showSelectionsForCurrentAccount() {
+        final long[] ids = mPerAccountCheckedIds.get(mAdapter.getAccount());
+        if (ids == null) {
+            selectAll();
+            return;
+        }
+        for (int i = 0, len = mListView.getCount(); i < len; i++) {
+            mListView.setItemChecked(i,
+                    Arrays.binarySearch(ids, mListView.getItemIdAtPosition(i)) >= 0);
+        }
+    }
+
+    private void selectAll() {
+        for (int i = 0, len = mListView.getCount(); i < len; i++) {
+            mListView.setItemChecked(i, true);
+        }
+    }
+
+    private void updateToolbarWithCurrentSelections() {
+        // The ListView keeps checked state for items that are disabled but we only want  to
+        // consider items that don't exist in the current account when updating the toolbar
+        int importableCount = 0;
+        final SparseBooleanArray checked = mListView.getCheckedItemPositions();
+        for (int i = 0; i < checked.size(); i++) {
+            if (checked.valueAt(i) && !mAdapter.existsInCurrentAccount(i)) {
+                importableCount++;
+            }
+        }
+
+        if (importableCount == 0) {
+            mImportButton.setVisibility(View.GONE);
+            mToolbar.setTitle(R.string.sim_import_title_none_selected);
+        } else {
+            mToolbar.setTitle(String.valueOf(importableCount));
+            mImportButton.setVisibility(View.VISIBLE);
+        }
+    }
+
     @Override
     public void onStart() {
         super.onStart();
@@ -195,6 +225,7 @@
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         mAccountHeaderPresenter.onSaveInstanceState(outState);
+        rememberSelectionsForCurrentAccount();
         saveAdapterSelectedStates(outState);
     }
 
@@ -207,11 +238,14 @@
     public void onLoadFinished(Loader<LoaderResult> loader,
             LoaderResult data) {
         mLoadingIndicator.hide();
-        mListView.setEmptyView(getView().findViewById(R.id.empty_message));
         if (data == null) {
             return;
         }
         mAdapter.setData(data);
+        mListView.setEmptyView(getView().findViewById(R.id.empty_message));
+
+        showSelectionsForCurrentAccount();
+        updateToolbarWithCurrentSelections();
     }
 
     @Override
@@ -227,9 +261,7 @@
         for (AccountWithDataSet account : accounts) {
             final long[] selections = savedInstanceState.getLongArray(
                     account.stringify() + KEY_SUFFIX_SELECTED_IDS);
-            if (selections != null) {
-                mAdapter.setSelectionsForAccount(account, selections);
-            }
+            mPerAccountCheckedIds.put(account, selections);
         }
     }
 
@@ -239,35 +271,34 @@
         }
 
         // Make sure the selections are up-to-date
-        mAdapter.storeCurrentSelections();
-        for (Map.Entry<AccountWithDataSet, TreeSet<Long>> entry :
-                mAdapter.getSelectedIds().entrySet()) {
-            final long[] ids = Longs.toArray(entry.getValue());
-            outState.putLongArray(entry.getKey().stringify() + KEY_SUFFIX_SELECTED_IDS, ids);
+        for (Map.Entry<AccountWithDataSet, long[]> entry : mPerAccountCheckedIds.entrySet()) {
+            outState.putLongArray(entry.getKey().stringify() + KEY_SUFFIX_SELECTED_IDS,
+                    entry.getValue());
         }
     }
 
     private void importCurrentSelections() {
+        final SparseBooleanArray checked = mListView.getCheckedItemPositions();
+        final ArrayList<SimContact> importableContacts = new ArrayList<>(checked.size());
+        for (int i = 0; i < checked.size(); i++) {
+            // It's possible for existing contacts to be "checked" but we only want to import the
+            // ones that don't already exist
+            if (checked.valueAt(i) && !mAdapter.existsInCurrentAccount(i)) {
+                importableContacts.add(mAdapter.getItem(i));
+            }
+        }
         ContactSaveService.startService(getContext(), ContactSaveService
                 .createImportFromSimIntent(getContext(), mSubscriptionId,
-                        mAdapter.getSelectedContacts(),
+                        importableContacts,
                         mAccountHeaderPresenter.getCurrentAccount()));
     }
 
-    @Override
-    public void onSelectedContactsChanged() {
-        updateSelectedCount();
-    }
-
-    private void updateSelectedCount() {
-        final int selectedCount = mAdapter.getSelectedContactIds().size();
-        if (selectedCount == 0) {
-            mToolbar.setTitle(R.string.sim_import_title_none_selected);
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        if (mAdapter.existsInCurrentAccount(position)) {
+            Snackbar.make(getView(), R.string.sim_import_contact_exists_toast,
+                    Snackbar.LENGTH_LONG).show();
         } else {
-            mToolbar.setTitle(String.valueOf(selectedCount));
-        }
-        if (mImportButton != null) {
-            mImportButton.setVisibility(selectedCount > 0 ? View.VISIBLE : View.GONE);
+            updateToolbarWithCurrentSelections();
         }
     }
 
@@ -315,108 +346,68 @@
         return fragment;
     }
 
-    private static class SimContactAdapter extends ContactListAdapter {
-        private ArrayList<SimContact> mContacts;
-        private AccountWithDataSet mSelectedAccount;
+    private static class SimContactAdapter extends ArrayAdapter<SimContact> {
         private Map<AccountWithDataSet, Set<SimContact>> mExistingMap;
-        private Map<AccountWithDataSet, TreeSet<Long>> mPerAccountCheckedIds = new ArrayMap<>();
-        private final int mCheckboxPaddingEnd;
+        private AccountWithDataSet mSelectedAccount;
+        private LayoutInflater mInflater;
 
         public SimContactAdapter(Context context) {
-            super(context);
-            mCheckboxPaddingEnd = context.getResources()
-                    .getDimensionPixelOffset(R.dimen.sim_import_checkbox_end_padding);
+            super(context, 0);
+            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         }
 
         @Override
-        public void configureLoader(CursorLoader loader, long directoryId) {
+        public long getItemId(int position) {
+            return getItem(position).getId();
         }
 
         @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);
+        public boolean hasStableIds() {
+            return true;
+        }
 
-            // For accessibility. Tapping the item checks this so we don't need it to be separately
-            // clickable
-            contactView.getCheckBox().setFocusable(false);
-            contactView.getCheckBox().setClickable(false);
-            // The default list pads the checkbox by a larger amount than we want.
-            contactView.setPaddingRelative(contactView.getPaddingStart(),
-                    contactView.getPaddingTop(), mCheckboxPaddingEnd,
-                    contactView.getPaddingBottom());
-            setViewEnabled(contactView, !existsInCurrentAccount(position));
+        @Override
+        public int getViewTypeCount() {
+            return 2;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return !existsInCurrentAccount(position) ? 0 : 1;
+        }
+
+        @NonNull
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            TextView text = (TextView) convertView;
+            if (text == null) {
+                final int layoutRes = existsInCurrentAccount(position) ?
+                        R.layout.sim_import_list_item_disabled :
+                        R.layout.sim_import_list_item;
+                text = (TextView) mInflater.inflate(layoutRes, parent, false);
+            }
+            text.setText(getItemLabel(getItem(position)));
+
+            return text;
         }
 
         public void setData(LoaderResult result) {
-            mContacts = result.contacts;
+            clear();
+            addAll(result.contacts);
             mExistingMap = result.accountsMap;
-            changeCursor(SimContact.convertToContactsCursor(mContacts,
-                    ContactQuery.CONTACT_PROJECTION_PRIMARY));
-            updateDisplayedSelections();
         }
 
         public void setAccount(AccountWithDataSet account) {
-            if (mContacts == null) {
-                mSelectedAccount = account;
-                return;
-            }
-
-            // Save the checked state for the current account.
-            storeCurrentSelections();
             mSelectedAccount = account;
-            updateDisplayedSelections();
-        }
-
-        public void storeCurrentSelections() {
-            if (mSelectedAccount != null) {
-                mPerAccountCheckedIds.put(mSelectedAccount, getSelectedContactIds());
-            }
-        }
-
-        public Map<AccountWithDataSet, TreeSet<Long>> getSelectedIds() {
-            return mPerAccountCheckedIds;
-        }
-
-        private void updateDisplayedSelections() {
-            if (mContacts == null) {
-                return;
-            }
-
-            TreeSet<Long> checked = mPerAccountCheckedIds.get(mSelectedAccount);
-            if (checked == null) {
-                checked = getEnabledIdsForCurrentAccount();
-                mPerAccountCheckedIds.put(mSelectedAccount, checked);
-            }
-            setSelectedContactIds(checked);
-
             notifyDataSetChanged();
         }
 
-        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 setSelectionsForAccount(AccountWithDataSet account, long[] contacts) {
-            final TreeSet<Long> selected = new TreeSet<>(Longs.asList(contacts));
-            mPerAccountCheckedIds.put(account, selected);
-            if (account.equals(mSelectedAccount)) {
-                updateDisplayedSelections();
-            }
+        public AccountWithDataSet getAccount() {
+            return mSelectedAccount;
         }
 
         public boolean existsInCurrentAccount(int position) {
-            return existsInCurrentAccount(mContacts.get(position));
+            return existsInCurrentAccount(getItem(position));
         }
 
         public boolean existsInCurrentAccount(SimContact contact) {
@@ -426,25 +417,17 @@
             return mExistingMap.get(mSelectedAccount).contains(contact);
         }
 
-        private TreeSet<Long> getEnabledIdsForCurrentAccount() {
-            final TreeSet<Long> result = new TreeSet<>();
-            for (SimContact contact : mContacts) {
-                if (!existsInCurrentAccount(contact)) {
-                    result.add(contact.getId());
-                }
+        private String getItemLabel(SimContact contact) {
+            if (contact.hasName()) {
+                return contact.getName();
+            } else if (contact.hasPhone()) {
+                return contact.getPhone();
+            } else if (contact.hasEmails()) {
+                return contact.getEmails()[0];
+            } else {
+                // This isn't really possible because we skip empty SIM contacts during loading
+                return "";
             }
-            return result;
-        }
-
-        private void setViewEnabled(ContactListItemView itemView, boolean enabled) {
-            itemView.getCheckBox().setEnabled(enabled);
-            itemView.getNameTextView().setEnabled(enabled);
-            // If the checkbox is left to default it's "unchecked" state will be announced when
-            // it is clicked on instead of the snackbar which is not useful.
-            int accessibilityMode = enabled ?
-                    View.IMPORTANT_FOR_ACCESSIBILITY_YES :
-                    View.IMPORTANT_FOR_ACCESSIBILITY_NO;
-            itemView.getCheckBox().setImportantForAccessibility(accessibilityMode);
         }
     }