Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 62b96291 authored by calderwoodra's avatar calderwoodra Committed by Copybara-Service
Browse files

Implemented SpeedDialUiItemLoader.

SpeedDialUiItemLoader builds a listenable future for returning
a list of SpeedDialUiItems which are the POJO representation of
each speed dial list element.

Bug: 36841782
Test: SpeedDialContentObserverTest
PiperOrigin-RevId: 192186376
Change-Id: I70f3abbeac14117ff4a68355e3a07b395b72386b
parent ff404929
Loading
Loading
Loading
Loading
+159 −0
Original line number Diff line number Diff line
/*
 * 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;

import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.speeddial.database.SpeedDialEntry;
import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * POJO representation of each speed dial list element.
 *
 * <p>Contains all data needed for the UI so that the UI never needs do additional contact queries.
 *
 * <p>Differs from {@link SpeedDialEntry} in that entries are specific to favorited/starred contacts
 * and {@link SpeedDialUiItem}s can be both favorites and suggested contacts.
 */
@AutoValue
public abstract class SpeedDialUiItem {

  public static final int LOOKUP_KEY = 0;
  public static final int CONTACT_ID = 1;
  public static final int DISPLAY_NAME = 2;
  public static final int STARRED = 3;
  public static final int NUMBER = 4;
  public static final int LABEL = 5;
  public static final int PHOTO_ID = 6;
  public static final int PHOTO_URI = 7;

  public static final String[] PHONE_PROJECTION = {
    Phone.LOOKUP_KEY,
    Phone.CONTACT_ID,
    Phone.DISPLAY_NAME,
    Phone.STARRED,
    Phone.NUMBER,
    Phone.LABEL,
    Phone.PHOTO_ID,
    Phone.PHOTO_URI
  };

  public static Builder builder() {
    return new AutoValue_SpeedDialUiItem.Builder().setChannels(ImmutableList.of());
  }

  /** Convert a cursor with projection {@link #PHONE_PROJECTION} into a {@link SpeedDialUiItem}. */
  public static SpeedDialUiItem fromCursor(Cursor cursor) {
    Assert.checkArgument(cursor != null);
    Assert.checkArgument(cursor.getCount() != 0);
    String lookupKey = cursor.getString(LOOKUP_KEY);
    SpeedDialUiItem.Builder builder =
        SpeedDialUiItem.builder()
            .setLookupKey(lookupKey)
            .setContactId(cursor.getLong(CONTACT_ID))
            // TODO(a bug): handle last name first preference
            .setName(cursor.getString(DISPLAY_NAME))
            .setIsStarred(cursor.getInt(STARRED) == 1)
            .setPhotoId(cursor.getLong(PHOTO_ID))
            .setPhotoUri(
                TextUtils.isEmpty(cursor.getString(PHOTO_URI)) ? "" : cursor.getString(PHOTO_URI));

    // While there are more rows and the lookup keys are the same, add a channel for each of the
    // contact's phone numbers.
    List<Channel> channels = new ArrayList<>();
    do {
      channels.add(
          Channel.builder()
              .setNumber(cursor.getString(NUMBER))
              .setLabel(TextUtils.isEmpty(cursor.getString(LABEL)) ? "" : cursor.getString(LABEL))
              // TODO(a bug): add another channel for each technology (Duo, ViLTE, ect.)
              .setTechnology(Channel.VOICE)
              .build());
    } while (cursor.moveToNext() && Objects.equals(lookupKey, cursor.getString(LOOKUP_KEY)));

    builder.setChannels(ImmutableList.copyOf(channels));
    return builder.build();
  }

  /** @see android.provider.ContactsContract.Contacts#DISPLAY_NAME */
  public abstract String name();

  /** @see android.provider.ContactsContract.Contacts#_ID */
  public abstract long contactId();

  /** @see android.provider.ContactsContract.Contacts#LOOKUP_KEY */
  public abstract String lookupKey();

  /** @see android.provider.ContactsContract.Contacts#STARRED */
  public abstract boolean isStarred();

  /** @see Phone#PHOTO_ID */
  public abstract long photoId();

  /** @see Phone#PHOTO_URI */
  public abstract String photoUri();

  /**
   * Since a contact can have multiple phone numbers and each number can have multiple technologies,
   * enumerate each one here so that the user can choose the correct one. Each channel here
   * represents a row in the {@link com.android.dialer.speeddial.DisambigDialog}.
   *
   * @see com.android.dialer.speeddial.database.SpeedDialEntry.Channel
   */
  public abstract ImmutableList<Channel> channels();

  /**
   * Will be null when the user hasn't chosen a default yet.
   *
   * @see com.android.dialer.speeddial.database.SpeedDialEntry#defaultChannel()
   */
  public abstract @Nullable Channel defaultChannel();

  public abstract Builder toBuilder();

  /** Builder class for speed dial contact. */
  @AutoValue.Builder
  public abstract static class Builder {

    public abstract Builder setName(String name);

    public abstract Builder setContactId(long contactId);

    public abstract Builder setLookupKey(String lookupKey);

    public abstract Builder setIsStarred(boolean isStarred);

    public abstract Builder setPhotoId(long photoId);

    public abstract Builder setPhotoUri(String photoUri);

    public abstract Builder setChannels(ImmutableList<Channel> channels);

    /** Set to null if the user hasn't chosen a default or the channel no longer exists. */
    public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel);

    public abstract SpeedDialUiItem build();
  }
}
+225 −0
Original line number Diff line number Diff line
/*
 * 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;

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.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;

/**
 * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}.
 *
 * @see #loadSpeedDialUiItems()
 *     <ol>
 *       <li>Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}.
 *       <li>Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in
 *           {@link Phone#CONTENT_URI}.
 *       <li>Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was
 *           deleted.
 *       <li>Update each {@link SpeedDialEntry} contact id, lookup key and channel.
 *       <li>Build a list of {@link SpeedDialUiItem} from {@link Contacts#STREQUENT_PHONE_ONLY}.
 *       <li>If any starred contacts in that list aren't in the {@link
 *           SpeedDialEntryDatabaseHelper}, insert them now.
 *       <li>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}.
 *     </ol>
 */
@SuppressWarnings("AndroidApiChecker")
@TargetApi(VERSION_CODES.N)
public final class SpeedDialUiItemLoader {

  private final Context appContext;
  private final ListeningExecutorService backgroundExecutor;

  @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}.
   */
  public ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() {
    return backgroundExecutor.submit(this::doInBackground);
  }

  @WorkerThread
  private ImmutableList<SpeedDialUiItem> doInBackground() {
    Assert.isWorkerThread();
    SpeedDialEntryDao db = new SpeedDialEntryDatabaseHelper(appContext);

    // This is the list of contacts that we will display to the user
    List<SpeedDialUiItem> speedDialUiItems = new ArrayList<>();

    // We'll use these lists to update the SpeedDialEntry database
    List<SpeedDialEntry> entriesToInsert = new ArrayList<>();
    List<SpeedDialEntry> entriesToUpdate = new ArrayList<>();
    List<Long> entriesToDelete = new ArrayList<>();

    // Track the highest entry ID
    // TODO(a bug): use auto-generated IDs
    long maxId = 0L;

    // Get all SpeedDialEntries and mark them to be updated or deleted
    List<SpeedDialEntry> entries = db.getAllEntries();
    for (SpeedDialEntry entry : entries) {
      maxId = Math.max(entry.id(), maxId);

      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<SpeedDialUiItem> 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(calderwoodra): set the defaults of these automatically
        speedDialUiItems.add(contact);

      } else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) {
        // Increment the ID so there aren't any collisions
        maxId += 1;
        entriesToInsert.add(
            SpeedDialEntry.builder()
                .setId(maxId)
                .setLookupKey(contact.lookupKey())
                .setContactId(contact.contactId())
                .setDefaultChannel(contact.defaultChannel())
                .build());

        // These are our newly starred contacts
        speedDialUiItems.add(contact);
      }
    }

    // TODO(a bug): use a single db transaction
    db.delete(entriesToDelete);
    db.update(entriesToUpdate);
    db.insert(entriesToInsert);
    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<SpeedDialUiItem> 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<SpeedDialUiItem> contacts = new ArrayList<>();
      if (cursor == null || cursor.getCount() == 0) {
        return contacts;
      }

      cursor.moveToPosition(-1);
      while (cursor.moveToNext()) {
        contacts.add(SpeedDialUiItem.fromCursor(cursor));
      }
      return contacts;
    }
  }
}
+8 −2
Original line number Diff line number Diff line
@@ -72,13 +72,17 @@ public abstract class SpeedDialEntry {

    public static final int UNKNOWN = 0;
    public static final int VOICE = 1;
    public static final int VIDEO = 2;
    public static final int IMS_VIDEO = 2;
    public static final int DUO = 3;

    /** Whether the Channel is for an audio or video call. */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({UNKNOWN, VOICE, VIDEO})
    @IntDef({UNKNOWN, VOICE, IMS_VIDEO, DUO})
    public @interface Technology {}

    public boolean isVideoTechnology() {
      return technology() == IMS_VIDEO || technology() == DUO;
    }
    /**
     * Raw phone number as the user entered it.
     *
@@ -96,6 +100,8 @@ public abstract class SpeedDialEntry {

    public abstract @Technology int technology();

    public abstract Builder toBuilder();

    public static Builder builder() {
      return new AutoValue_SpeedDialEntry_Channel.Builder();
    }
+5 −1
Original line number Diff line number Diff line
@@ -18,7 +18,11 @@ package com.android.dialer.speeddial.database;

import java.util.List;

/** Interface that databases support speed dial entries should implement. */
/**
 * Interface that databases support speed dial entries should implement.
 *
 * <p>This database is only used for favorite/starred contacts.
 */
public interface SpeedDialEntryDao {

  /** Return all entries in the database */
+19 −13
Original line number Diff line number Diff line
@@ -28,7 +28,11 @@ import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
import java.util.ArrayList;
import java.util.List;

/** {@link SpeedDialEntryDao} implemented as an SQLite database. */
/**
 * {@link SpeedDialEntryDao} implemented as an SQLite database.
 *
 * @see SpeedDialEntryDao
 */
public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
    implements SpeedDialEntryDao {

@@ -42,7 +46,7 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
  private static final String LOOKUP_KEY = "lookup_key";
  private static final String PHONE_NUMBER = "phone_number";
  private static final String PHONE_LABEL = "phone_label";
  private static final String PHONE_TYPE = "phone_type";
  private static final String PHONE_TECHNOLOGY = "phone_technology";

  // Column positions
  private static final int POSITION_ID = 0;
@@ -50,7 +54,7 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
  private static final int POSITION_LOOKUP_KEY = 2;
  private static final int POSITION_PHONE_NUMBER = 3;
  private static final int POSITION_PHONE_LABEL = 4;
  private static final int POSITION_PHONE_TYPE = 5;
  private static final int POSITION_PHONE_TECHNOLOGY = 5;

  // Create Table Query
  private static final String CREATE_TABLE_SQL =
@@ -62,7 +66,7 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
          + (LOOKUP_KEY + " text, ")
          + (PHONE_NUMBER + " text, ")
          + (PHONE_LABEL + " text, ")
          + (PHONE_TYPE + " integer ")
          + (PHONE_TECHNOLOGY + " integer ")
          + ");";

  private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME;
@@ -98,15 +102,17 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
        Cursor cursor = db.rawQuery(query, null)) {
      cursor.moveToPosition(-1);
      while (cursor.moveToNext()) {
        Channel channel =
        String number = cursor.getString(POSITION_PHONE_NUMBER);
        Channel channel = null;
        if (!TextUtils.isEmpty(number)) {
          channel =
              Channel.builder()
                .setNumber(cursor.getString(POSITION_PHONE_NUMBER))
                  .setNumber(number)
                  .setLabel(cursor.getString(POSITION_PHONE_LABEL))
                .setTechnology(cursor.getInt(POSITION_PHONE_TYPE))
                  .setTechnology(cursor.getInt(POSITION_PHONE_TECHNOLOGY))
                  .build();
        if (TextUtils.isEmpty(channel.number())) {
          channel = null;
        }

        SpeedDialEntry entry =
            SpeedDialEntry.builder()
                .setDefaultChannel(channel)
@@ -183,7 +189,7 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
    if (entry.defaultChannel() != null) {
      values.put(PHONE_NUMBER, entry.defaultChannel().number());
      values.put(PHONE_LABEL, entry.defaultChannel().label());
      values.put(PHONE_TYPE, entry.defaultChannel().technology());
      values.put(PHONE_TECHNOLOGY, entry.defaultChannel().technology());
    }
    return values;
  }