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 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 onCreate(final Bundle savedInstanceState)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 onActivityCreated(Bundle savedInstanceState)110 public void onActivityCreated(Bundle savedInstanceState) { 111 super.onActivityCreated(savedInstanceState); 112 getLoaderManager().initLoader(0, null, this); 113 } 114 115 @Nullable 116 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 rememberSelectionsForCurrentAccount()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 showSelectionsForCurrentAccount()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 selectAll()196 private void selectAll() { 197 for (int i = 0, len = mListView.getCount(); i < len; i++) { 198 mListView.setItemChecked(i, true); 199 } 200 } 201 updateToolbarWithCurrentSelections()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 onStart()223 public void onStart() { 224 super.onStart(); 225 if (mAdapter.isEmpty() && getLoaderManager().getLoader(0).isStarted()) { 226 mLoadingIndicator.show(); 227 } 228 } 229 230 @Override onSaveInstanceState(Bundle outState)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 onCreateLoader(int id, Bundle args)241 public Loader<LoaderResult> onCreateLoader(int id, Bundle args) { 242 return new SimContactLoader(getContext(), mSubscriptionId); 243 } 244 245 @Override onLoadFinished(Loader<LoaderResult> loader, LoaderResult data)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 onLoaderReset(Loader<LoaderResult> loader)262 public void onLoaderReset(Loader<LoaderResult> loader) { 263 } 264 restoreAdapterSelectedStates(List<AccountInfo> accounts)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 saveAdapterSelectedStates(Bundle outState)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 importCurrentSelections()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 onItemClick(AdapterView<?> parent, View view, int position, long id)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 getContext()313 public Context getContext() { 314 if (CompatUtils.isMarshmallowCompatible()) { 315 return super.getContext(); 316 } 317 return getActivity(); 318 } 319 320 @Override onScrollStateChanged(AbsListView view, int scrollState)321 public void onScrollStateChanged(AbsListView view, int scrollState) { } 322 323 @Override onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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 */ newInstance()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 */ newInstance(int subscriptionId)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 SimContactAdapter(Context context)362 public SimContactAdapter(Context context) { 363 super(context, 0); 364 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 365 } 366 367 @Override getItemId(int position)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 hasStableIds()378 public boolean hasStableIds() { 379 return true; 380 } 381 382 @Override getViewTypeCount()383 public int getViewTypeCount() { 384 return 2; 385 } 386 387 @Override getItemViewType(int position)388 public int getItemViewType(int position) { 389 return !existsInCurrentAccount(position) ? 0 : 1; 390 } 391 392 @NonNull 393 @Override getView(int position, View convertView, ViewGroup parent)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 setData(LoaderResult result)407 public void setData(LoaderResult result) { 408 clear(); 409 addAll(result.contacts); 410 mExistingMap = result.accountsMap; 411 } 412 setAccount(AccountWithDataSet account)413 public void setAccount(AccountWithDataSet account) { 414 mSelectedAccount = account; 415 notifyDataSetChanged(); 416 } 417 getAccount()418 public AccountWithDataSet getAccount() { 419 return mSelectedAccount; 420 } 421 existsInCurrentAccount(int position)422 public boolean existsInCurrentAccount(int position) { 423 return existsInCurrentAccount(getItem(position)); 424 } 425 existsInCurrentAccount(SimContact contact)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 getItemLabel(SimContact contact)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 SimContactLoader(Context context, int subscriptionId)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 loadData()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 loadFromSim()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