1 /* 2 * Copyright (C) 2018 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 17 package com.android.dialer.speeddial.loader; 18 19 import android.content.ContentProviderOperation; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.OperationApplicationException; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.RemoteException; 26 import android.os.Trace; 27 import android.provider.ContactsContract; 28 import android.provider.ContactsContract.CommonDataKinds.Phone; 29 import android.provider.ContactsContract.Contacts; 30 import android.support.annotation.MainThread; 31 import android.support.annotation.WorkerThread; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import com.android.dialer.common.Assert; 35 import com.android.dialer.common.LogUtil; 36 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 37 import com.android.dialer.common.concurrent.DefaultFutureCallback; 38 import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; 39 import com.android.dialer.common.concurrent.DialerFutureSerializer; 40 import com.android.dialer.common.database.Selection; 41 import com.android.dialer.contacts.ContactsComponent; 42 import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences; 43 import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.DisplayOrder; 44 import com.android.dialer.contacts.hiresphoto.HighResolutionPhotoRequester; 45 import com.android.dialer.duo.DuoComponent; 46 import com.android.dialer.inject.ApplicationContext; 47 import com.android.dialer.speeddial.database.SpeedDialEntry; 48 import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; 49 import com.android.dialer.speeddial.database.SpeedDialEntryDao; 50 import com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper; 51 import com.android.dialer.util.CallUtil; 52 import com.google.common.base.Optional; 53 import com.google.common.collect.ImmutableList; 54 import com.google.common.collect.ImmutableMap; 55 import com.google.common.util.concurrent.Futures; 56 import com.google.common.util.concurrent.ListenableFuture; 57 import com.google.common.util.concurrent.ListeningExecutorService; 58 import com.google.common.util.concurrent.MoreExecutors; 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.Map; 62 import java.util.Objects; 63 import java.util.Set; 64 import javax.inject.Inject; 65 import javax.inject.Singleton; 66 67 /** 68 * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}. 69 * 70 * @see #loadSpeedDialUiItems() 71 * <ol> 72 * <li>Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}. 73 * <li>Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in 74 * {@link Phone#CONTENT_URI}. 75 * <li>Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was 76 * deleted. 77 * <li>Update each {@link SpeedDialEntry} contact id, lookup key and channel. 78 * <li>Build a list of {@link SpeedDialUiItem} from starred contacts. 79 * <li>If any contacts in that list aren't in the {@link SpeedDialEntryDatabaseHelper}, insert 80 * them now. 81 * <li>Notify the {@link SuccessListener} of the complete list of {@link SpeedDialUiItem 82 * SpeedDialContacts} composed from {@link SpeedDialEntry SpeedDialEntries} and 83 * non-starred {@link Contacts#STREQUENT_PHONE_ONLY}. 84 * </ol> 85 */ 86 @Singleton 87 public final class SpeedDialUiItemMutator { 88 89 private final Context appContext; 90 private final ListeningExecutorService backgroundExecutor; 91 // Used to ensure that only one refresh flow runs at a time. 92 private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); 93 private final ContactDisplayPreferences contactDisplayPreferences; 94 private final HighResolutionPhotoRequester highResolutionPhotoRequester; 95 96 @Inject SpeedDialUiItemMutator( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor, ContactDisplayPreferences contactDisplayPreferences, HighResolutionPhotoRequester highResolutionPhotoRequester)97 public SpeedDialUiItemMutator( 98 @ApplicationContext Context appContext, 99 @BackgroundExecutor ListeningExecutorService backgroundExecutor, 100 ContactDisplayPreferences contactDisplayPreferences, 101 HighResolutionPhotoRequester highResolutionPhotoRequester) { 102 this.appContext = appContext; 103 this.backgroundExecutor = backgroundExecutor; 104 this.contactDisplayPreferences = contactDisplayPreferences; 105 this.highResolutionPhotoRequester = highResolutionPhotoRequester; 106 } 107 108 /** 109 * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This 110 * list is composed of starred contacts from {@link SpeedDialEntryDatabaseHelper}. 111 */ loadSpeedDialUiItems()112 public ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() { 113 return dialerFutureSerializer.submit(this::loadSpeedDialUiItemsInternal, backgroundExecutor); 114 } 115 116 /** 117 * Delete the SpeedDialUiItem. 118 * 119 * <p>If the item is starred, it's entry will be removed from the SpeedDialEntry database. 120 * Additionally, if the contact only has one entry in the database, it will be unstarred. 121 * 122 * <p>If the item isn't starred, it's usage data will be deleted. 123 * 124 * @return the updated list of SpeedDialUiItems. 125 */ removeSpeedDialUiItem( SpeedDialUiItem speedDialUiItem)126 public ListenableFuture<ImmutableList<SpeedDialUiItem>> removeSpeedDialUiItem( 127 SpeedDialUiItem speedDialUiItem) { 128 return dialerFutureSerializer.submit( 129 () -> removeSpeedDialUiItemInternal(speedDialUiItem), backgroundExecutor); 130 } 131 132 @WorkerThread removeSpeedDialUiItemInternal( SpeedDialUiItem speedDialUiItem)133 private ImmutableList<SpeedDialUiItem> removeSpeedDialUiItemInternal( 134 SpeedDialUiItem speedDialUiItem) { 135 Assert.isWorkerThread(); 136 Assert.checkArgument(speedDialUiItem.isStarred()); 137 removeStarredSpeedDialUiItem(speedDialUiItem); 138 return loadSpeedDialUiItemsInternal(); 139 } 140 141 /** 142 * Delete the SpeedDialEntry associated with the passed in SpeedDialUiItem. Additionally, if the 143 * entry being deleted is the only entry for that contact, unstar it in the cp2. 144 */ 145 @WorkerThread removeStarredSpeedDialUiItem(SpeedDialUiItem speedDialUiItem)146 private void removeStarredSpeedDialUiItem(SpeedDialUiItem speedDialUiItem) { 147 Assert.isWorkerThread(); 148 Assert.checkArgument(speedDialUiItem.isStarred()); 149 SpeedDialEntryDao db = getSpeedDialEntryDao(); 150 ImmutableList<SpeedDialEntry> entries = db.getAllEntries(); 151 152 SpeedDialEntry entryToDelete = null; 153 int entriesForTheSameContact = 0; 154 for (SpeedDialEntry entry : entries) { 155 if (entry.contactId() == speedDialUiItem.contactId()) { 156 entriesForTheSameContact++; 157 } 158 159 if (Objects.equals(entry.id(), speedDialUiItem.speedDialEntryId())) { 160 Assert.checkArgument(entryToDelete == null); 161 entryToDelete = entry; 162 } 163 } 164 db.delete(ImmutableList.of(entryToDelete.id())); 165 if (entriesForTheSameContact == 1) { 166 unstarContact(speedDialUiItem); 167 } 168 } 169 170 @WorkerThread unstarContact(SpeedDialUiItem speedDialUiItem)171 private void unstarContact(SpeedDialUiItem speedDialUiItem) { 172 Assert.isWorkerThread(); 173 ContentValues contentValues = new ContentValues(); 174 contentValues.put(Phone.STARRED, 0); 175 appContext 176 .getContentResolver() 177 .update( 178 Contacts.CONTENT_URI, 179 contentValues, 180 Contacts._ID + " = ?", 181 new String[] {Long.toString(speedDialUiItem.contactId())}); 182 } 183 184 /** 185 * Takes a contact uri from {@link Phone#CONTENT_URI} and updates {@link Phone#STARRED} to be 186 * true, if it isn't already or Inserts the contact into the {@link SpeedDialEntryDatabaseHelper} 187 */ starContact(Uri contactUri)188 public ListenableFuture<ImmutableList<SpeedDialUiItem>> starContact(Uri contactUri) { 189 return dialerFutureSerializer.submit( 190 () -> insertNewContactEntry(contactUri), backgroundExecutor); 191 } 192 193 @WorkerThread insertNewContactEntry(Uri contactUri)194 private ImmutableList<SpeedDialUiItem> insertNewContactEntry(Uri contactUri) { 195 Assert.isWorkerThread(); 196 try (Cursor cursor = 197 appContext 198 .getContentResolver() 199 .query( 200 contactUri, 201 SpeedDialUiItem.getPhoneProjection(isPrimaryDisplayNameOrder()), 202 null, 203 null, 204 null)) { 205 if (cursor == null) { 206 LogUtil.e("SpeedDialUiItemMutator.insertNewContactEntry", "Cursor was null"); 207 return loadSpeedDialUiItemsInternal(); 208 } 209 Assert.checkArgument(cursor.moveToFirst(), "Cursor should never be empty"); 210 SpeedDialUiItem item = 211 SpeedDialUiItem.fromCursor( 212 appContext.getResources(), cursor, CallUtil.isVideoEnabled(appContext)); 213 214 // Star the contact if it isn't starred already, then return. 215 if (!item.isStarred()) { 216 ContentValues values = new ContentValues(); 217 values.put(Phone.STARRED, "1"); 218 appContext 219 .getContentResolver() 220 .update( 221 Contacts.CONTENT_URI, 222 values, 223 Contacts._ID + " = ?", 224 new String[] {Long.toString(item.contactId())}); 225 } 226 227 // Insert a new entry into the SpeedDialEntry database 228 getSpeedDialEntryDao().insert(item.buildSpeedDialEntry()); 229 } 230 return loadSpeedDialUiItemsInternal(); 231 } 232 233 @WorkerThread loadSpeedDialUiItemsInternal()234 private ImmutableList<SpeedDialUiItem> loadSpeedDialUiItemsInternal() { 235 Trace.beginSection("loadSpeedDialUiItemsInternal"); 236 Assert.isWorkerThread(); 237 Trace.beginSection("getAllEntries"); 238 SpeedDialEntryDao db = getSpeedDialEntryDao(); 239 Trace.endSection(); // getAllEntries 240 241 // This is the list of contacts that we will display to the user 242 List<SpeedDialUiItem> speedDialUiItems = new ArrayList<>(); 243 244 // We'll use these lists to update the SpeedDialEntry database 245 List<SpeedDialEntry> entriesToInsert = new ArrayList<>(); 246 List<SpeedDialEntry> entriesToUpdate = new ArrayList<>(); 247 List<Long> entriesToDelete = new ArrayList<>(); 248 249 // Get all SpeedDialEntries and update their contact ids and lookupkeys. 250 List<SpeedDialEntry> entries = db.getAllEntries(); 251 entries = updateContactIdsAndLookupKeys(entries); 252 253 // Build SpeedDialUiItems from our updated entries. 254 Map<SpeedDialEntry, SpeedDialUiItem> entriesToUiItems = getSpeedDialUiItemsFromEntries(entries); 255 Assert.checkArgument( 256 entries.size() == entriesToUiItems.size(), 257 "Updated entries are incomplete: " + entries.size() + " != " + entriesToUiItems.size()); 258 259 // Mark the SpeedDialEntries to be updated or deleted 260 Trace.beginSection("updateOrDeleteEntries"); 261 for (SpeedDialEntry entry : entries) { 262 SpeedDialUiItem contact = entriesToUiItems.get(entry); 263 // Remove contacts that no longer exist or are no longer starred 264 if (contact == null || !contact.isStarred()) { 265 entriesToDelete.add(entry.id()); 266 continue; 267 } 268 269 // Contact exists, so update its entry in SpeedDialEntry Database 270 entriesToUpdate.add( 271 entry 272 .toBuilder() 273 .setLookupKey(contact.lookupKey()) 274 .setContactId(contact.contactId()) 275 .setDefaultChannel(contact.defaultChannel()) 276 .build()); 277 278 // These are our existing starred entries 279 speedDialUiItems.add(contact); 280 } 281 Trace.endSection(); // updateOrDeleteEntries 282 283 // Get all starred contacts 284 List<SpeedDialUiItem> starredContacts = getStarredContacts(); 285 // If it is starred and not already accounted for above, then insert into the SpeedDialEntry DB. 286 Trace.beginSection("addStarredContact"); 287 for (SpeedDialUiItem contact : starredContacts) { 288 if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) { 289 entriesToInsert.add(contact.buildSpeedDialEntry()); 290 291 // These are our newly starred contacts 292 speedDialUiItems.add(contact); 293 } 294 } 295 Trace.endSection(); // addStarredContact 296 297 Trace.beginSection("insertUpdateAndDelete"); 298 requestHighResolutionPhoto(entriesToInsert); 299 ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap = 300 db.insertUpdateAndDelete( 301 ImmutableList.copyOf(entriesToInsert), 302 ImmutableList.copyOf(entriesToUpdate), 303 ImmutableList.copyOf(entriesToDelete)); 304 Trace.endSection(); // insertUpdateAndDelete 305 Trace.endSection(); // loadSpeedDialUiItemsInternal 306 return speedDialUiItemsWithUpdatedIds(speedDialUiItems, insertedEntriesToIdsMap); 307 } 308 309 @WorkerThread requestHighResolutionPhoto(List<SpeedDialEntry> newEntries)310 private void requestHighResolutionPhoto(List<SpeedDialEntry> newEntries) { 311 ContactsComponent.get(appContext).highResolutionPhotoLoader(); 312 for (SpeedDialEntry entry : newEntries) { 313 Uri uri; 314 uri = Contacts.getLookupUri(entry.contactId(), entry.lookupKey()); 315 316 Futures.addCallback( 317 highResolutionPhotoRequester.request(uri), 318 new DefaultFutureCallback<>(), 319 MoreExecutors.directExecutor()); 320 } 321 } 322 323 /** 324 * Since newly starred contacts sometimes aren't in the SpeedDialEntry database, we couldn't set 325 * their ids when we created our initial list of {@link SpeedDialUiItem speedDialUiItems}. Now 326 * that we've inserted the entries into the database and we have their ids, build a new list of 327 * speedDialUiItems with the now known ids. 328 */ speedDialUiItemsWithUpdatedIds( List<SpeedDialUiItem> speedDialUiItems, ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap)329 private ImmutableList<SpeedDialUiItem> speedDialUiItemsWithUpdatedIds( 330 List<SpeedDialUiItem> speedDialUiItems, 331 ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap) { 332 if (insertedEntriesToIdsMap.isEmpty()) { 333 // There were no newly inserted entries, so all entries ids are set already. 334 return ImmutableList.copyOf(speedDialUiItems); 335 } 336 337 ImmutableList.Builder<SpeedDialUiItem> updatedItems = ImmutableList.builder(); 338 for (SpeedDialUiItem speedDialUiItem : speedDialUiItems) { 339 SpeedDialEntry entry = speedDialUiItem.buildSpeedDialEntry(); 340 if (insertedEntriesToIdsMap.containsKey(entry)) { 341 // Get the id for newly inserted entry, update our SpeedDialUiItem and add it to our list 342 Long id = Assert.isNotNull(insertedEntriesToIdsMap.get(entry)); 343 updatedItems.add(speedDialUiItem.toBuilder().setSpeedDialEntryId(id).build()); 344 continue; 345 } 346 347 // Starred contacts that aren't in the map, should already have speed dial entry ids. 348 // Non-starred contacts aren't in the speed dial entry database, so they 349 // shouldn't have speed dial entry ids. 350 Assert.checkArgument( 351 speedDialUiItem.isStarred() == (speedDialUiItem.speedDialEntryId() != null), 352 "Contact must be starred with a speed dial entry id, or not starred with no id " 353 + "(suggested contacts)"); 354 updatedItems.add(speedDialUiItem); 355 } 356 return updatedItems.build(); 357 } 358 359 /** 360 * Returns the same list of SpeedDialEntries that are passed in except their contact ids and 361 * lookup keys are updated to current values. 362 * 363 * <p>Unfortunately, we need to look up each contact individually to update the contact id and 364 * lookup key. Luckily though, this query is highly optimized on the framework side and very 365 * quick. 366 */ 367 @WorkerThread updateContactIdsAndLookupKeys(List<SpeedDialEntry> entries)368 private List<SpeedDialEntry> updateContactIdsAndLookupKeys(List<SpeedDialEntry> entries) { 369 Assert.isWorkerThread(); 370 List<SpeedDialEntry> updatedEntries = new ArrayList<>(); 371 for (SpeedDialEntry entry : entries) { 372 try (Cursor cursor = 373 appContext 374 .getContentResolver() 375 .query( 376 Contacts.getLookupUri(entry.contactId(), entry.lookupKey()), 377 new String[] {Contacts._ID, Contacts.LOOKUP_KEY}, 378 null, 379 null, 380 null)) { 381 if (cursor == null) { 382 LogUtil.e("SpeedDialUiItemMutator.updateContactIdsAndLookupKeys", "null cursor"); 383 return new ArrayList<>(); 384 } 385 if (cursor.getCount() == 0) { 386 // No need to update this entry, the contact was deleted. We'll clear it up later. 387 updatedEntries.add(entry); 388 continue; 389 } 390 // Since all cursor rows will be have the same contact id and lookup key, just grab the 391 // first one. 392 cursor.moveToFirst(); 393 updatedEntries.add( 394 entry 395 .toBuilder() 396 .setContactId(cursor.getLong(0)) 397 .setLookupKey(cursor.getString(1)) 398 .build()); 399 } 400 } 401 return updatedEntries; 402 } 403 404 /** 405 * Returns a map of SpeedDialEntries to their corresponding SpeedDialUiItems. Mappings to null 406 * elements imply that the contact was deleted. 407 */ 408 @WorkerThread getSpeedDialUiItemsFromEntries( List<SpeedDialEntry> entries)409 private Map<SpeedDialEntry, SpeedDialUiItem> getSpeedDialUiItemsFromEntries( 410 List<SpeedDialEntry> entries) { 411 Trace.beginSection("getSpeedDialUiItemsFromEntries"); 412 Assert.isWorkerThread(); 413 // Fetch the contact ids from the SpeedDialEntries 414 Set<String> contactIds = new ArraySet<>(); 415 entries.forEach(entry -> contactIds.add(Long.toString(entry.contactId()))); 416 if (contactIds.isEmpty()) { 417 Trace.endSection(); 418 return new ArrayMap<>(); 419 } 420 421 // Build SpeedDialUiItems from those contact ids and map them to their entries 422 Selection selection = 423 Selection.builder().and(Selection.column(Phone.CONTACT_ID).in(contactIds)).build(); 424 try (Cursor cursor = 425 appContext 426 .getContentResolver() 427 .query( 428 Phone.CONTENT_URI, 429 SpeedDialUiItem.getPhoneProjection(isPrimaryDisplayNameOrder()), 430 selection.getSelection(), 431 selection.getSelectionArgs(), 432 null)) { 433 Map<SpeedDialEntry, SpeedDialUiItem> map = new ArrayMap<>(); 434 for (cursor.moveToFirst(); !cursor.isAfterLast(); /* Iterate in the loop */ ) { 435 SpeedDialUiItem item = 436 SpeedDialUiItem.fromCursor( 437 appContext.getResources(), cursor, CallUtil.isVideoEnabled(appContext)); 438 for (SpeedDialEntry entry : entries) { 439 if (entry.contactId() == item.contactId()) { 440 // Update the id and pinned position to match it's corresponding SpeedDialEntry. 441 SpeedDialUiItem.Builder entrySpeedDialItem = 442 item.toBuilder() 443 .setSpeedDialEntryId(entry.id()) 444 .setPinnedPosition(entry.pinnedPosition()); 445 446 // Preserve the default channel if it didn't change/still exists 447 Channel defaultChannel = entry.defaultChannel(); 448 if (defaultChannel != null) { 449 if (item.channels().contains(defaultChannel) 450 || isValidDuoDefaultChannel(item.channels(), defaultChannel)) { 451 entrySpeedDialItem.setDefaultChannel(defaultChannel); 452 } 453 } 454 455 // It's impossible for two contacts to exist with the same contact id, so if this entry 456 // was previously matched to a SpeedDialUiItem and is being matched again, something 457 // went horribly wrong. 458 Assert.checkArgument( 459 map.put(entry, entrySpeedDialItem.build()) == null, 460 "Each SpeedDialEntry only has one correct SpeedDialUiItem"); 461 } 462 } 463 } 464 465 // Contact must have been deleted 466 for (SpeedDialEntry entry : entries) { 467 map.putIfAbsent(entry, null); 468 } 469 Trace.endSection(); 470 return map; 471 } 472 } 473 474 /** 475 * Since we can't check duo reachabliity on background threads, we have to assume the contact is 476 * still duo reachable. So we just check it is and return true if the Duo number is still 477 * associated with the contact. 478 */ isValidDuoDefaultChannel( ImmutableList<Channel> channels, Channel defaultChannel)479 private static boolean isValidDuoDefaultChannel( 480 ImmutableList<Channel> channels, Channel defaultChannel) { 481 if (defaultChannel.technology() != Channel.DUO) { 482 return false; 483 } 484 485 for (Channel channel : channels) { 486 if (channel.number().equals(defaultChannel.number())) { 487 return true; 488 } 489 } 490 return false; 491 } 492 493 @WorkerThread getStarredContacts()494 private List<SpeedDialUiItem> getStarredContacts() { 495 Trace.beginSection("getStrequentContacts"); 496 Assert.isWorkerThread(); 497 Set<String> contactIds = new ArraySet<>(); 498 499 // Fetch the contact ids of all starred contacts 500 Uri strequentUri = 501 Contacts.CONTENT_STREQUENT_URI 502 .buildUpon() 503 .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") 504 .build(); 505 Selection selection = Selection.column(Phone.STARRED).is("=", 1); 506 try (Cursor cursor = 507 appContext 508 .getContentResolver() 509 .query( 510 strequentUri, 511 new String[] {Phone.CONTACT_ID}, 512 selection.getSelection(), 513 selection.getSelectionArgs(), 514 null)) { 515 if (cursor == null) { 516 LogUtil.e("SpeedDialUiItemMutator.getStarredContacts", "null cursor"); 517 Trace.endSection(); 518 return new ArrayList<>(); 519 } 520 if (cursor.getCount() == 0) { 521 Trace.endSection(); 522 return new ArrayList<>(); 523 } 524 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 525 contactIds.add(Long.toString(cursor.getLong(0))); 526 } 527 } 528 529 // Build SpeedDialUiItems from those contact ids 530 selection = Selection.builder().and(Selection.column(Phone.CONTACT_ID).in(contactIds)).build(); 531 try (Cursor cursor = 532 appContext 533 .getContentResolver() 534 .query( 535 Phone.CONTENT_URI, 536 SpeedDialUiItem.getPhoneProjection(isPrimaryDisplayNameOrder()), 537 selection.getSelection(), 538 selection.getSelectionArgs(), 539 null)) { 540 List<SpeedDialUiItem> contacts = new ArrayList<>(); 541 if (cursor == null) { 542 LogUtil.e("SpeedDialUiItemMutator.getStrequentContacts", "null cursor"); 543 Trace.endSection(); 544 return new ArrayList<>(); 545 } 546 if (cursor.getCount() == 0) { 547 Trace.endSection(); 548 return contacts; 549 } 550 for (cursor.moveToFirst(); !cursor.isAfterLast(); /* Iterate in the loop */ ) { 551 contacts.add( 552 SpeedDialUiItem.fromCursor( 553 appContext.getResources(), cursor, CallUtil.isVideoEnabled(appContext))); 554 } 555 Trace.endSection(); 556 return contacts; 557 } 558 } 559 560 /** 561 * Persists the position of the {@link SpeedDialUiItem items} as the pinned position according to 562 * the order they were passed in. 563 */ 564 @WorkerThread updatePinnedPosition(List<SpeedDialUiItem> speedDialUiItems)565 public void updatePinnedPosition(List<SpeedDialUiItem> speedDialUiItems) { 566 Assert.isWorkerThread(); 567 if (speedDialUiItems == null || speedDialUiItems.isEmpty()) { 568 return; 569 } 570 571 // Update the positions in the SpeedDialEntry database 572 ImmutableList.Builder<SpeedDialEntry> entriesToUpdate = ImmutableList.builder(); 573 for (int i = 0; i < speedDialUiItems.size(); i++) { 574 SpeedDialUiItem item = speedDialUiItems.get(i); 575 if (item.isStarred()) { 576 entriesToUpdate.add( 577 item.buildSpeedDialEntry().toBuilder().setPinnedPosition(Optional.of(i)).build()); 578 } 579 } 580 getSpeedDialEntryDao().update(entriesToUpdate.build()); 581 582 // Update the positions in CP2 583 // Build a list of SpeedDialUiItems where each contact is only represented once but the order 584 // is maintained. For example, assume you have a list of contacts with contact ids: 585 // > { 1, 1, 2, 1, 2, 3 } 586 // This list will be reduced to: 587 // > { 1, 2, 3 } 588 // and their positions in the resulting list will be written to the CP2 Contacts.PINNED column. 589 List<SpeedDialUiItem> cp2SpeedDialUiItems = new ArrayList<>(); 590 Set<Long> contactIds = new ArraySet<>(); 591 for (SpeedDialUiItem item : speedDialUiItems) { 592 if (contactIds.add(item.contactId())) { 593 cp2SpeedDialUiItems.add(item); 594 } 595 } 596 597 // Code copied from PhoneFavoritesTileAdapter#handleDrop 598 ArrayList<ContentProviderOperation> operations = new ArrayList<>(); 599 for (int i = 0; i < cp2SpeedDialUiItems.size(); i++) { 600 SpeedDialUiItem item = cp2SpeedDialUiItems.get(i); 601 // Pinned positions in the database start from 1 instead of being zero-indexed like 602 // arrays, so offset by 1. 603 int databasePinnedPosition = i + 1; 604 if (item.pinnedPosition().isPresent() 605 && item.pinnedPosition().get() == databasePinnedPosition) { 606 continue; 607 } 608 609 Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(item.contactId())); 610 ContentValues values = new ContentValues(); 611 values.put(Contacts.PINNED, databasePinnedPosition); 612 operations.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 613 } 614 if (operations.isEmpty()) { 615 // Nothing to update 616 return; 617 } 618 try { 619 appContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); 620 // TODO(calderwoodra): log 621 } catch (RemoteException | OperationApplicationException e) { 622 LogUtil.e( 623 "SpeedDialUiItemMutator.updatePinnedPosition", 624 "Exception thrown when pinning contacts", 625 e); 626 } 627 } 628 629 /** 630 * Returns a new list with duo reachable channels inserted. Duo channels won't replace ViLTE 631 * channels. 632 */ 633 @MainThread insertDuoChannels( Context context, ImmutableList<SpeedDialUiItem> speedDialUiItems)634 public ImmutableList<SpeedDialUiItem> insertDuoChannels( 635 Context context, ImmutableList<SpeedDialUiItem> speedDialUiItems) { 636 Assert.isMainThread(); 637 638 ImmutableList.Builder<SpeedDialUiItem> newSpeedDialItemList = ImmutableList.builder(); 639 // for each existing item 640 for (SpeedDialUiItem item : speedDialUiItems) { 641 if (item.defaultChannel() == null) { 642 // If the contact is starred and doesn't have a default channel, insert duo channels 643 newSpeedDialItemList.add(insertDuoChannelsToStarredContact(context, item)); 644 } else { 645 // if starred and has a default channel, leave it as is, the user knows what they want. 646 newSpeedDialItemList.add(item); 647 } 648 } 649 return newSpeedDialItemList.build(); 650 } 651 652 @MainThread insertDuoChannelsToStarredContact(Context context, SpeedDialUiItem item)653 private SpeedDialUiItem insertDuoChannelsToStarredContact(Context context, SpeedDialUiItem item) { 654 Assert.isMainThread(); 655 Assert.checkArgument(item.isStarred()); 656 657 // build a new list of channels 658 ImmutableList.Builder<Channel> newChannelsList = ImmutableList.builder(); 659 Channel previousChannel = item.channels().get(0); 660 newChannelsList.add(previousChannel); 661 662 for (int i = 1; i < item.channels().size(); i++) { 663 Channel currentChannel = item.channels().get(i); 664 // If the previous and current channel are voice channels, that means the previous number 665 // didn't have a video channel. 666 // If the previous number is duo reachable, insert a duo channel. 667 if (!previousChannel.isVideoTechnology() 668 && !currentChannel.isVideoTechnology() 669 && DuoComponent.get(context).getDuo().isReachable(context, previousChannel.number())) { 670 newChannelsList.add(previousChannel.toBuilder().setTechnology(Channel.DUO).build()); 671 } 672 newChannelsList.add(currentChannel); 673 previousChannel = currentChannel; 674 } 675 676 // Check the last channel 677 if (!previousChannel.isVideoTechnology() 678 && DuoComponent.get(context).getDuo().isReachable(context, previousChannel.number())) { 679 newChannelsList.add(previousChannel.toBuilder().setTechnology(Channel.DUO).build()); 680 } 681 return item.toBuilder().setChannels(newChannelsList.build()).build(); 682 } 683 getSpeedDialEntryDao()684 private SpeedDialEntryDao getSpeedDialEntryDao() { 685 return new SpeedDialEntryDatabaseHelper(appContext); 686 } 687 isPrimaryDisplayNameOrder()688 private boolean isPrimaryDisplayNameOrder() { 689 return contactDisplayPreferences.getDisplayOrder() == DisplayOrder.PRIMARY; 690 } 691 } 692