Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts;
     17 
     18 import android.app.Activity;
     19 import android.app.Fragment;
     20 import android.app.LoaderManager;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.IntentFilter;
     24 import android.content.Loader;
     25 import android.os.Bundle;
     26 import android.support.annotation.NonNull;
     27 import android.support.annotation.Nullable;
     28 import android.support.design.widget.Snackbar;
     29 import android.support.v4.content.LocalBroadcastManager;
     30 import android.support.v4.util.ArrayMap;
     31 import android.support.v4.view.ViewCompat;
     32 import android.support.v4.widget.ContentLoadingProgressBar;
     33 import android.support.v7.widget.Toolbar;
     34 import android.util.SparseBooleanArray;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.widget.AbsListView;
     39 import android.widget.AdapterView;
     40 import android.widget.ArrayAdapter;
     41 import android.widget.ListView;
     42 import android.widget.TextView;
     43 
     44 import com.android.contacts.compat.CompatUtils;
     45 import com.android.contacts.database.SimContactDao;
     46 import com.android.contacts.editor.AccountHeaderPresenter;
     47 import com.android.contacts.model.AccountTypeManager;
     48 import com.android.contacts.model.SimCard;
     49 import com.android.contacts.model.SimContact;
     50 import com.android.contacts.model.account.AccountInfo;
     51 import com.android.contacts.model.account.AccountWithDataSet;
     52 import com.android.contacts.preference.ContactsPreferences;
     53 import com.android.contacts.util.concurrent.ContactsExecutors;
     54 import com.android.contacts.util.concurrent.ListenableFutureLoader;
     55 import com.google.common.base.Function;
     56 import com.google.common.util.concurrent.Futures;
     57 import com.google.common.util.concurrent.ListenableFuture;
     58 
     59 import java.util.ArrayList;
     60 import java.util.Arrays;
     61 import java.util.Collections;
     62 import java.util.List;
     63 import java.util.Map;
     64 import java.util.Set;
     65 import java.util.concurrent.Callable;
     66 
     67 /**
     68  * Dialog that presents a list of contacts from a SIM card that can be imported into a selected
     69  * account
     70  */
     71 public class SimImportFragment extends Fragment
     72         implements LoaderManager.LoaderCallbacks<SimImportFragment.LoaderResult>,
     73         AdapterView.OnItemClickListener, AbsListView.OnScrollListener {
     74 
     75     private static final String KEY_SUFFIX_SELECTED_IDS = "_selectedIds";
     76     private static final String ARG_SUBSCRIPTION_ID = "subscriptionId";
     77 
     78     private ContactsPreferences mPreferences;
     79     private AccountTypeManager mAccountTypeManager;
     80     private SimContactAdapter mAdapter;
     81     private View mAccountHeaderContainer;
     82     private AccountHeaderPresenter mAccountHeaderPresenter;
     83     private float mAccountScrolledElevationPixels;
     84     private ContentLoadingProgressBar mLoadingIndicator;
     85     private Toolbar mToolbar;
     86     private ListView mListView;
     87     private View mImportButton;
     88 
     89     private Bundle mSavedInstanceState;
     90 
     91     private final Map<AccountWithDataSet, long[]> mPerAccountCheckedIds = new ArrayMap<>();
     92 
     93     private int mSubscriptionId;
     94 
     95     @Override
     96     public void onCreate(final Bundle savedInstanceState) {
     97         super.onCreate(savedInstanceState);
     98 
     99         mSavedInstanceState = savedInstanceState;
    100         mPreferences = new ContactsPreferences(getContext());
    101         mAccountTypeManager = AccountTypeManager.getInstance(getActivity());
    102         mAdapter = new SimContactAdapter(getActivity());
    103 
    104         final Bundle args = getArguments();
    105         mSubscriptionId = args == null ? SimCard.NO_SUBSCRIPTION_ID :
    106                 args.getInt(ARG_SUBSCRIPTION_ID, SimCard.NO_SUBSCRIPTION_ID);
    107     }
    108 
    109     @Override
    110     public void onActivityCreated(Bundle savedInstanceState) {
    111         super.onActivityCreated(savedInstanceState);
    112         getLoaderManager().initLoader(0, null, this);
    113     }
    114 
    115     @Nullable
    116     @Override
    117     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    118             Bundle savedInstanceState) {
    119         final View view = inflater.inflate(R.layout.fragment_sim_import, container, false);
    120 
    121         mAccountHeaderContainer = view.findViewById(R.id.account_header_container);
    122         mAccountScrolledElevationPixels = getResources()
    123                 .getDimension(R.dimen.contact_list_header_elevation);
    124         mAccountHeaderPresenter = new AccountHeaderPresenter(
    125                 mAccountHeaderContainer);
    126         if (savedInstanceState != null) {
    127             mAccountHeaderPresenter.onRestoreInstanceState(savedInstanceState);
    128         } else {
    129             // Default may be null in which case the first account in the list will be selected
    130             // after they are loaded.
    131             mAccountHeaderPresenter.setCurrentAccount(mPreferences.getDefaultAccount());
    132         }
    133         mAccountHeaderPresenter.setObserver(new AccountHeaderPresenter.Observer() {
    134             @Override
    135             public void onChange(AccountHeaderPresenter sender) {
    136                 rememberSelectionsForCurrentAccount();
    137                 mAdapter.setAccount(sender.getCurrentAccount());
    138                 showSelectionsForCurrentAccount();
    139                 updateToolbarWithCurrentSelections();
    140             }
    141         });
    142         mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount());
    143 
    144         mListView = (ListView) view.findViewById(R.id.list);
    145         mListView.setOnScrollListener(this);
    146         mListView.setAdapter(mAdapter);
    147         mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
    148         mListView.setOnItemClickListener(this);
    149         mImportButton = view.findViewById(R.id.import_button);
    150         mImportButton.setOnClickListener(new View.OnClickListener() {
    151             @Override
    152             public void onClick(View v) {
    153                 importCurrentSelections();
    154                 // Do we wait for import to finish?
    155                 getActivity().setResult(Activity.RESULT_OK);
    156                 getActivity().finish();
    157             }
    158         });
    159 
    160         mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
    161         mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
    162             @Override
    163             public void onClick(View v) {
    164                 getActivity().setResult(Activity.RESULT_CANCELED);
    165                 getActivity().finish();
    166             }
    167         });
    168 
    169         mLoadingIndicator = (ContentLoadingProgressBar) view.findViewById(R.id.loading_progress);
    170 
    171         return view;
    172     }
    173 
    174     private void rememberSelectionsForCurrentAccount() {
    175         final AccountWithDataSet current = mAdapter.getAccount();
    176         if (current == null) {
    177             return;
    178         }
    179         final long[] ids = mListView.getCheckedItemIds();
    180         Arrays.sort(ids);
    181         mPerAccountCheckedIds.put(current, ids);
    182     }
    183 
    184     private void showSelectionsForCurrentAccount() {
    185         final long[] ids = mPerAccountCheckedIds.get(mAdapter.getAccount());
    186         if (ids == null) {
    187             selectAll();
    188             return;
    189         }
    190         for (int i = 0, len = mListView.getCount(); i < len; i++) {
    191             mListView.setItemChecked(i,
    192                     Arrays.binarySearch(ids, mListView.getItemIdAtPosition(i)) >= 0);
    193         }
    194     }
    195 
    196     private void selectAll() {
    197         for (int i = 0, len = mListView.getCount(); i < len; i++) {
    198             mListView.setItemChecked(i, true);
    199         }
    200     }
    201 
    202     private void updateToolbarWithCurrentSelections() {
    203         // The ListView keeps checked state for items that are disabled but we only want  to
    204         // consider items that don't exist in the current account when updating the toolbar
    205         int importableCount = 0;
    206         final SparseBooleanArray checked = mListView.getCheckedItemPositions();
    207         for (int i = 0; i < checked.size(); i++) {
    208             if (checked.valueAt(i) && !mAdapter.existsInCurrentAccount(checked.keyAt(i))) {
    209                 importableCount++;
    210             }
    211         }
    212 
    213         if (importableCount == 0) {
    214             mImportButton.setVisibility(View.GONE);
    215             mToolbar.setTitle(R.string.sim_import_title_none_selected);
    216         } else {
    217             mToolbar.setTitle(String.valueOf(importableCount));
    218             mImportButton.setVisibility(View.VISIBLE);
    219         }
    220     }
    221 
    222     @Override
    223     public void onStart() {
    224         super.onStart();
    225         if (mAdapter.isEmpty() && getLoaderManager().getLoader(0).isStarted()) {
    226             mLoadingIndicator.show();
    227         }
    228     }
    229 
    230     @Override
    231     public void onSaveInstanceState(Bundle outState) {
    232         rememberSelectionsForCurrentAccount();
    233         // We'll restore this manually so we don't need the list to preserve it's own state.
    234         mListView.clearChoices();
    235         super.onSaveInstanceState(outState);
    236         mAccountHeaderPresenter.onSaveInstanceState(outState);
    237         saveAdapterSelectedStates(outState);
    238     }
    239 
    240     @Override
    241     public Loader<LoaderResult> onCreateLoader(int id, Bundle args) {
    242         return new SimContactLoader(getContext(), mSubscriptionId);
    243     }
    244 
    245     @Override
    246     public void onLoadFinished(Loader<LoaderResult> loader,
    247             LoaderResult data) {
    248         mLoadingIndicator.hide();
    249         if (data == null) {
    250             return;
    251         }
    252         mAccountHeaderPresenter.setAccounts(data.accounts);
    253         restoreAdapterSelectedStates(data.accounts);
    254         mAdapter.setData(data);
    255         mListView.setEmptyView(getView().findViewById(R.id.empty_message));
    256 
    257         showSelectionsForCurrentAccount();
    258         updateToolbarWithCurrentSelections();
    259     }
    260 
    261     @Override
    262     public void onLoaderReset(Loader<LoaderResult> loader) {
    263     }
    264 
    265     private void restoreAdapterSelectedStates(List<AccountInfo> accounts) {
    266         if (mSavedInstanceState == null) {
    267             return;
    268         }
    269 
    270         for (AccountInfo account : accounts) {
    271             final long[] selections = mSavedInstanceState.getLongArray(
    272                     account.getAccount().stringify() + KEY_SUFFIX_SELECTED_IDS);
    273             mPerAccountCheckedIds.put(account.getAccount(), selections);
    274         }
    275         mSavedInstanceState = null;
    276     }
    277 
    278     private void saveAdapterSelectedStates(Bundle outState) {
    279         if (mAdapter == null) {
    280             return;
    281         }
    282 
    283         // Make sure the selections are up-to-date
    284         for (Map.Entry<AccountWithDataSet, long[]> entry : mPerAccountCheckedIds.entrySet()) {
    285             outState.putLongArray(entry.getKey().stringify() + KEY_SUFFIX_SELECTED_IDS,
    286                     entry.getValue());
    287         }
    288     }
    289 
    290     private void importCurrentSelections() {
    291         final SparseBooleanArray checked = mListView.getCheckedItemPositions();
    292         final ArrayList<SimContact> importableContacts = new ArrayList<>(checked.size());
    293         for (int i = 0; i < checked.size(); i++) {
    294             // It's possible for existing contacts to be "checked" but we only want to import the
    295             // ones that don't already exist
    296             if (checked.valueAt(i) && !mAdapter.existsInCurrentAccount(i)) {
    297                 importableContacts.add(mAdapter.getItem(checked.keyAt(i)));
    298             }
    299         }
    300         SimImportService.startImport(getContext(), mSubscriptionId, importableContacts,
    301                 mAccountHeaderPresenter.getCurrentAccount());
    302     }
    303 
    304     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    305         if (mAdapter.existsInCurrentAccount(position)) {
    306             Snackbar.make(getView(), R.string.sim_import_contact_exists_toast,
    307                     Snackbar.LENGTH_LONG).show();
    308         } else {
    309             updateToolbarWithCurrentSelections();
    310         }
    311     }
    312 
    313     public Context getContext() {
    314         if (CompatUtils.isMarshmallowCompatible()) {
    315             return super.getContext();
    316         }
    317         return getActivity();
    318     }
    319 
    320     @Override
    321     public void onScrollStateChanged(AbsListView view, int scrollState) { }
    322 
    323     @Override
    324     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    325             int totalItemCount) {
    326         int firstCompletelyVisibleItem = firstVisibleItem;
    327         if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) {
    328             firstCompletelyVisibleItem++;
    329         }
    330 
    331         if (firstCompletelyVisibleItem == 0) {
    332             ViewCompat.setElevation(mAccountHeaderContainer, 0);
    333         } else {
    334             ViewCompat.setElevation(mAccountHeaderContainer, mAccountScrolledElevationPixels);
    335         }
    336     }
    337 
    338     /**
    339      * Creates a fragment that will display contacts stored on the default SIM card
    340      */
    341     public static SimImportFragment newInstance() {
    342         return new SimImportFragment();
    343     }
    344 
    345     /**
    346      * Creates a fragment that will display the contacts stored on the SIM card that has the
    347      * provided subscriptionId
    348      */
    349     public static SimImportFragment newInstance(int subscriptionId) {
    350         final SimImportFragment fragment = new SimImportFragment();
    351         final Bundle args = new Bundle();
    352         args.putInt(ARG_SUBSCRIPTION_ID, subscriptionId);
    353         fragment.setArguments(args);
    354         return fragment;
    355     }
    356 
    357     private static class SimContactAdapter extends ArrayAdapter<SimContact> {
    358         private Map<AccountWithDataSet, Set<SimContact>> mExistingMap;
    359         private AccountWithDataSet mSelectedAccount;
    360         private LayoutInflater mInflater;
    361 
    362         public SimContactAdapter(Context context) {
    363             super(context, 0);
    364             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    365         }
    366 
    367         @Override
    368         public long getItemId(int position) {
    369             // This can be called by the framework when the adapter hasn't been initialized for
    370             // checking the checked state of items. See b/33108913
    371             if (position < 0 || position >= getCount()) {
    372                 return View.NO_ID;
    373             }
    374             return getItem(position).getId();
    375         }
    376 
    377         @Override
    378         public boolean hasStableIds() {
    379             return true;
    380         }
    381 
    382         @Override
    383         public int getViewTypeCount() {
    384             return 2;
    385         }
    386 
    387         @Override
    388         public int getItemViewType(int position) {
    389             return !existsInCurrentAccount(position) ? 0 : 1;
    390         }
    391 
    392         @NonNull
    393         @Override
    394         public View getView(int position, View convertView, ViewGroup parent) {
    395             TextView text = (TextView) convertView;
    396             if (text == null) {
    397                 final int layoutRes = existsInCurrentAccount(position) ?
    398                         R.layout.sim_import_list_item_disabled :
    399                         R.layout.sim_import_list_item;
    400                 text = (TextView) mInflater.inflate(layoutRes, parent, false);
    401             }
    402             text.setText(getItemLabel(getItem(position)));
    403 
    404             return text;
    405         }
    406 
    407         public void setData(LoaderResult result) {
    408             clear();
    409             addAll(result.contacts);
    410             mExistingMap = result.accountsMap;
    411         }
    412 
    413         public void setAccount(AccountWithDataSet account) {
    414             mSelectedAccount = account;
    415             notifyDataSetChanged();
    416         }
    417 
    418         public AccountWithDataSet getAccount() {
    419             return mSelectedAccount;
    420         }
    421 
    422         public boolean existsInCurrentAccount(int position) {
    423             return existsInCurrentAccount(getItem(position));
    424         }
    425 
    426         public boolean existsInCurrentAccount(SimContact contact) {
    427             if (mSelectedAccount == null || !mExistingMap.containsKey(mSelectedAccount)) {
    428                 return false;
    429             }
    430             return mExistingMap.get(mSelectedAccount).contains(contact);
    431         }
    432 
    433         private String getItemLabel(SimContact contact) {
    434             if (contact.hasName()) {
    435                 return contact.getName();
    436             } else if (contact.hasPhone()) {
    437                 return contact.getPhone();
    438             } else if (contact.hasEmails()) {
    439                 return contact.getEmails()[0];
    440             } else {
    441                 // This isn't really possible because we skip empty SIM contacts during loading
    442                 return "";
    443             }
    444         }
    445     }
    446 
    447 
    448     private static class SimContactLoader extends ListenableFutureLoader<LoaderResult> {
    449         private SimContactDao mDao;
    450         private AccountTypeManager mAccountTypeManager;
    451         private final int mSubscriptionId;
    452 
    453         public SimContactLoader(Context context, int subscriptionId) {
    454             super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
    455             mDao = SimContactDao.create(context);
    456             mAccountTypeManager = AccountTypeManager.getInstance(getContext());
    457             mSubscriptionId = subscriptionId;
    458         }
    459 
    460         @Override
    461         protected ListenableFuture<LoaderResult> loadData() {
    462             final ListenableFuture<List<Object>> future = Futures.<Object>allAsList(
    463                     mAccountTypeManager
    464                             .filterAccountsAsync(AccountTypeManager.writableFilter()),
    465                     ContactsExecutors.getSimReadExecutor().<Object>submit(
    466                             new Callable<Object>() {
    467                         @Override
    468                         public LoaderResult call() throws Exception {
    469                             return loadFromSim();
    470                         }
    471                     }));
    472             return Futures.transform(future, new Function<List<Object>, LoaderResult>() {
    473                 @Override
    474                 public LoaderResult apply(List<Object> input) {
    475                     final List<AccountInfo> accounts = (List<AccountInfo>) input.get(0);
    476                     final LoaderResult simLoadResult = (LoaderResult) input.get(1);
    477                     simLoadResult.accounts = accounts;
    478                     return simLoadResult;
    479                 }
    480             });
    481         }
    482 
    483         private LoaderResult loadFromSim() {
    484             final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId);
    485             LoaderResult result = new LoaderResult();
    486             if (sim == null) {
    487                 result.contacts = new ArrayList<>();
    488                 result.accountsMap = Collections.emptyMap();
    489                 return result;
    490             }
    491             result.contacts = mDao.loadContactsForSim(sim);
    492             result.accountsMap = mDao.findAccountsOfExistingSimContacts(result.contacts);
    493             return result;
    494         }
    495     }
    496 
    497     public static class LoaderResult {
    498         public List<AccountInfo> accounts;
    499         public ArrayList<SimContact> contacts;
    500         public Map<AccountWithDataSet, Set<SimContact>> accountsMap;
    501     }
    502 }
    503