/* * 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.Activity; import android.app.Fragment; import android.app.LoaderManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.IntentFilter; import android.content.Loader; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.content.LocalBroadcastManager; 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.compat.CompatUtils; import com.android.contacts.database.SimContactDao; import com.android.contacts.editor.AccountHeaderPresenter; import com.android.contacts.model.AccountTypeManager; import com.android.contacts.model.SimCard; import com.android.contacts.model.SimContact; import com.android.contacts.model.account.AccountInfo; import com.android.contacts.model.account.AccountWithDataSet; import com.android.contacts.preference.ContactsPreferences; import com.android.contacts.util.concurrent.ContactsExecutors; import com.android.contacts.util.concurrent.ListenableFutureLoader; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; 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.concurrent.Callable; /** * Dialog that presents a list of contacts from a SIM card that can be imported into a selected * account */ public class SimImportFragment extends Fragment implements LoaderManager.LoaderCallbacks, AdapterView.OnItemClickListener, AbsListView.OnScrollListener { private static final String KEY_SUFFIX_SELECTED_IDS = "_selectedIds"; private static final String ARG_SUBSCRIPTION_ID = "subscriptionId"; private ContactsPreferences mPreferences; private AccountTypeManager mAccountTypeManager; private SimContactAdapter mAdapter; private View mAccountHeaderContainer; private AccountHeaderPresenter mAccountHeaderPresenter; private float mAccountScrolledElevationPixels; private ContentLoadingProgressBar mLoadingIndicator; private Toolbar mToolbar; private ListView mListView; private View mImportButton; private Bundle mSavedInstanceState; private final Map mPerAccountCheckedIds = new ArrayMap<>(); private int mSubscriptionId; @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSavedInstanceState = savedInstanceState; mPreferences = new ContactsPreferences(getContext()); mAccountTypeManager = AccountTypeManager.getInstance(getActivity()); mAdapter = new SimContactAdapter(getActivity()); final Bundle args = getArguments(); mSubscriptionId = args == null ? SimCard.NO_SUBSCRIPTION_ID : args.getInt(ARG_SUBSCRIPTION_ID, SimCard.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); mAccountHeaderContainer = view.findViewById(R.id.account_header_container); mAccountScrolledElevationPixels = getResources() .getDimension(R.dimen.contact_list_header_elevation); mAccountHeaderPresenter = new AccountHeaderPresenter( mAccountHeaderContainer); if (savedInstanceState != null) { mAccountHeaderPresenter.onRestoreInstanceState(savedInstanceState); } else { // Default may be null in which case the first account in the list will be selected // after they are loaded. mAccountHeaderPresenter.setCurrentAccount(mPreferences.getDefaultAccount()); } mAccountHeaderPresenter.setObserver(new AccountHeaderPresenter.Observer() { @Override public void onChange(AccountHeaderPresenter sender) { rememberSelectionsForCurrentAccount(); mAdapter.setAccount(sender.getCurrentAccount()); showSelectionsForCurrentAccount(); updateToolbarWithCurrentSelections(); } }); mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount()); mListView = (ListView) view.findViewById(R.id.list); mListView.setOnScrollListener(this); mListView.setAdapter(mAdapter); mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); mListView.setOnItemClickListener(this); 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? getActivity().setResult(Activity.RESULT_OK); getActivity().finish(); } }); mToolbar = (Toolbar) view.findViewById(R.id.toolbar); mToolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getActivity().setResult(Activity.RESULT_CANCELED); getActivity().finish(); } }); mLoadingIndicator = (ContentLoadingProgressBar) view.findViewById(R.id.loading_progress); return view; } private void rememberSelectionsForCurrentAccount() { final AccountWithDataSet current = mAdapter.getAccount(); if (current == null) { return; } 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(checked.keyAt(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(); if (mAdapter.isEmpty() && getLoaderManager().getLoader(0).isStarted()) { mLoadingIndicator.show(); } } @Override public void onSaveInstanceState(Bundle outState) { rememberSelectionsForCurrentAccount(); // We'll restore this manually so we don't need the list to preserve it's own state. mListView.clearChoices(); super.onSaveInstanceState(outState); mAccountHeaderPresenter.onSaveInstanceState(outState); saveAdapterSelectedStates(outState); } @Override public Loader onCreateLoader(int id, Bundle args) { return new SimContactLoader(getContext(), mSubscriptionId); } @Override public void onLoadFinished(Loader loader, LoaderResult data) { mLoadingIndicator.hide(); if (data == null) { return; } mAccountHeaderPresenter.setAccounts(data.accounts); restoreAdapterSelectedStates(data.accounts); mAdapter.setData(data); mListView.setEmptyView(getView().findViewById(R.id.empty_message)); showSelectionsForCurrentAccount(); updateToolbarWithCurrentSelections(); } @Override public void onLoaderReset(Loader loader) { } private void restoreAdapterSelectedStates(List accounts) { if (mSavedInstanceState == null) { return; } for (AccountInfo account : accounts) { final long[] selections = mSavedInstanceState.getLongArray( account.getAccount().stringify() + KEY_SUFFIX_SELECTED_IDS); mPerAccountCheckedIds.put(account.getAccount(), selections); } mSavedInstanceState = null; } private void saveAdapterSelectedStates(Bundle outState) { if (mAdapter == null) { return; } // Make sure the selections are up-to-date for (Map.Entry entry : mPerAccountCheckedIds.entrySet()) { outState.putLongArray(entry.getKey().stringify() + KEY_SUFFIX_SELECTED_IDS, entry.getValue()); } } private void importCurrentSelections() { final SparseBooleanArray checked = mListView.getCheckedItemPositions(); final ArrayList 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(checked.keyAt(i))); } } SimImportService.startImport(getContext(), mSubscriptionId, importableContacts, mAccountHeaderPresenter.getCurrentAccount()); } 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 { updateToolbarWithCurrentSelections(); } } public Context getContext() { if (CompatUtils.isMarshmallowCompatible()) { return super.getContext(); } return getActivity(); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { int firstCompletelyVisibleItem = firstVisibleItem; if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) { firstCompletelyVisibleItem++; } if (firstCompletelyVisibleItem == 0) { ViewCompat.setElevation(mAccountHeaderContainer, 0); } else { ViewCompat.setElevation(mAccountHeaderContainer, mAccountScrolledElevationPixels); } } /** * 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 ArrayAdapter { private Map> mExistingMap; private AccountWithDataSet mSelectedAccount; private LayoutInflater mInflater; public SimContactAdapter(Context context) { super(context, 0); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public long getItemId(int position) { // This can be called by the framework when the adapter hasn't been initialized for // checking the checked state of items. See b/33108913 if (position < 0 || position >= getCount()) { return View.NO_ID; } return getItem(position).getId(); } @Override public boolean hasStableIds() { return true; } @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) { clear(); addAll(result.contacts); mExistingMap = result.accountsMap; } public void setAccount(AccountWithDataSet account) { mSelectedAccount = account; notifyDataSetChanged(); } public AccountWithDataSet getAccount() { return mSelectedAccount; } public boolean existsInCurrentAccount(int position) { return existsInCurrentAccount(getItem(position)); } public boolean existsInCurrentAccount(SimContact contact) { if (mSelectedAccount == null || !mExistingMap.containsKey(mSelectedAccount)) { return false; } return mExistingMap.get(mSelectedAccount).contains(contact); } 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 ""; } } } private static class SimContactLoader extends ListenableFutureLoader { private SimContactDao mDao; private AccountTypeManager mAccountTypeManager; private final int mSubscriptionId; public SimContactLoader(Context context, int subscriptionId) { super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED)); mDao = SimContactDao.create(context); mAccountTypeManager = AccountTypeManager.getInstance(getContext()); mSubscriptionId = subscriptionId; } @Override protected ListenableFuture loadData() { final ListenableFuture> future = Futures.allAsList( mAccountTypeManager .filterAccountsAsync(AccountTypeManager.writableFilter()), ContactsExecutors.getSimReadExecutor().submit( new Callable() { @Override public LoaderResult call() throws Exception { return loadFromSim(); } })); return Futures.transform(future, new Function, LoaderResult>() { @Override public LoaderResult apply(List input) { final List accounts = (List) input.get(0); final LoaderResult simLoadResult = (LoaderResult) input.get(1); simLoadResult.accounts = accounts; return simLoadResult; } }); } private LoaderResult loadFromSim() { final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId); LoaderResult result = new LoaderResult(); if (sim == null) { result.contacts = new ArrayList<>(); result.accountsMap = Collections.emptyMap(); return result; } result.contacts = mDao.loadContactsForSim(sim); result.accountsMap = mDao.findAccountsOfExistingSimContacts(result.contacts); return result; } } public static class LoaderResult { public List accounts; public ArrayList contacts; public Map> accountsMap; } }