/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.dialer.speeddial.loader; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build.VERSION_CODES; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.support.annotation.WorkerThread; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; import com.android.dialer.common.concurrent.DialerFutureSerializer; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.speeddial.database.SpeedDialEntry; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.android.dialer.speeddial.database.SpeedDialEntryDao; import com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; /** * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}. * * @see #loadSpeedDialUiItems() *
    *
  1. Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}. *
  2. Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in * {@link Phone#CONTENT_URI}. *
  3. Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was * deleted. *
  4. Update each {@link SpeedDialEntry} contact id, lookup key and channel. *
  5. Build a list of {@link SpeedDialUiItem} from {@link Contacts#STREQUENT_PHONE_ONLY}. *
  6. If any starred contacts in that list aren't in the {@link * SpeedDialEntryDatabaseHelper}, insert them now. *
  7. Notify the {@link SuccessListener} of the complete list of {@link SpeedDialUiItem * SpeedDialContacts} composed from {@link SpeedDialEntry SpeedDialEntries} and * non-starred {@link Contacts#STREQUENT_PHONE_ONLY}. *
*/ @SuppressWarnings("AndroidApiChecker") @TargetApi(VERSION_CODES.N) @Singleton public final class SpeedDialUiItemLoader implements UiItemLoader { private final Context appContext; private final ListeningExecutorService backgroundExecutor; // Used to ensure that only one refresh flow runs at a time. private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); @Inject public SpeedDialUiItemLoader( @ApplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor) { this.appContext = appContext; this.backgroundExecutor = backgroundExecutor; } /** * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This * list is composed of starred contacts from {@link SpeedDialEntryDatabaseHelper} and suggestions * from {@link Contacts#STREQUENT_PHONE_ONLY}. */ @Override public ListenableFuture> loadSpeedDialUiItems() { return dialerFutureSerializer.submitAsync( () -> backgroundExecutor.submit(this::doInBackground), backgroundExecutor); } @WorkerThread private ImmutableList doInBackground() { Assert.isWorkerThread(); SpeedDialEntryDao db = new SpeedDialEntryDatabaseHelper(appContext); // This is the list of contacts that we will display to the user List speedDialUiItems = new ArrayList<>(); // We'll use these lists to update the SpeedDialEntry database List entriesToInsert = new ArrayList<>(); List entriesToUpdate = new ArrayList<>(); List entriesToDelete = new ArrayList<>(); // Get all SpeedDialEntries and mark them to be updated or deleted List entries = db.getAllEntries(); for (SpeedDialEntry entry : entries) { SpeedDialUiItem contact = getSpeedDialContact(entry); // Remove contacts that no longer exist or are no longer starred if (contact == null || !contact.isStarred()) { entriesToDelete.add(entry.id()); continue; } // Contact exists, so update its entry in SpeedDialEntry Database entriesToUpdate.add( entry .toBuilder() .setLookupKey(contact.lookupKey()) .setContactId(contact.contactId()) .setDefaultChannel(contact.defaultChannel()) .build()); // These are our existing starred entries speedDialUiItems.add(contact); } // Get all Strequent Contacts List strequentContacts = getStrequentContacts(); // For each contact, if it isn't starred, add it as a suggestion. // If it is starred and not already accounted for above, then insert into the SpeedDialEntry DB. for (SpeedDialUiItem contact : strequentContacts) { if (!contact.isStarred()) { // Add this contact as a suggestion // TODO(77754534): improve suggestions beyond just first channel speedDialUiItems.add( contact.toBuilder().setDefaultChannel(contact.channels().get(0)).build()); } else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) { entriesToInsert.add( SpeedDialEntry.builder() .setLookupKey(contact.lookupKey()) .setContactId(contact.contactId()) .setDefaultChannel(contact.defaultChannel()) .build()); // These are our newly starred contacts speedDialUiItems.add(contact); } } db.insertUpdateAndDelete( ImmutableList.copyOf(entriesToInsert), ImmutableList.copyOf(entriesToUpdate), ImmutableList.copyOf(entriesToDelete)); return ImmutableList.copyOf(speedDialUiItems); } @WorkerThread private SpeedDialUiItem getSpeedDialContact(SpeedDialEntry entry) { Assert.isWorkerThread(); // TODO(b77725860): Might need to use the lookup uri to get the contact id first, then query // based on that. SpeedDialUiItem contact; try (Cursor cursor = appContext .getContentResolver() .query( Phone.CONTENT_URI, SpeedDialUiItem.PHONE_PROJECTION, Phone.NUMBER + " IS NOT NULL AND " + Phone.LOOKUP_KEY + "=?", new String[] {entry.lookupKey()}, null)) { if (cursor == null || cursor.getCount() == 0) { // Contact not found, potentially deleted LogUtil.e("SpeedDialUiItemLoader.getSpeedDialContact", "Contact not found."); return null; } cursor.moveToFirst(); contact = SpeedDialUiItem.fromCursor(cursor); } // Preserve the default channel if it didn't change/still exists Channel defaultChannel = entry.defaultChannel(); if (defaultChannel != null) { if (contact.channels().contains(defaultChannel)) { contact = contact.toBuilder().setDefaultChannel(defaultChannel).build(); } } // TODO(calderwoodra): Consider setting the default channel if there is only one channel return contact; } @WorkerThread private List getStrequentContacts() { Assert.isWorkerThread(); Uri uri = Contacts.CONTENT_STREQUENT_URI .buildUpon() .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") .build(); try (Cursor cursor = appContext .getContentResolver() .query(uri, SpeedDialUiItem.PHONE_PROJECTION, null, null, null)) { List contacts = new ArrayList<>(); if (cursor == null || cursor.getCount() == 0) { return contacts; } cursor.moveToPosition(-1); while (cursor.moveToNext()) { contacts.add(SpeedDialUiItem.fromCursor(cursor)); } return contacts; } } }