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