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