summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/speeddial
diff options
context:
space:
mode:
authorcalderwoodra <calderwoodra@google.com>2018-04-09 14:16:47 -0700
committerCopybara-Service <copybara-piper@google.com>2018-04-09 14:49:20 -0700
commit62b96291dd2bf025f0f84ba0376301a31a6b8776 (patch)
treebc639247443482f8f708552e201cf0978e441298 /java/com/android/dialer/speeddial
parentff404929afbea0bacce10512b8a5adc9ab28e509 (diff)
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
Diffstat (limited to 'java/com/android/dialer/speeddial')
-rw-r--r--java/com/android/dialer/speeddial/SpeedDialUiItem.java159
-rw-r--r--java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java225
-rw-r--r--java/com/android/dialer/speeddial/database/SpeedDialEntry.java10
-rw-r--r--java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java6
-rw-r--r--java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java32
5 files changed, 416 insertions, 16 deletions
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItem.java b/java/com/android/dialer/speeddial/SpeedDialUiItem.java
new file mode 100644
index 000000000..17552adf7
--- /dev/null
+++ b/java/com/android/dialer/speeddial/SpeedDialUiItem.java
@@ -0,0 +1,159 @@
+/*
+ * 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();
+ }
+}
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
new file mode 100644
index 000000000..257c74f58
--- /dev/null
+++ b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
@@ -0,0 +1,225 @@
+/*
+ * 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;
+ }
+ }
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
index aa90909f1..f63619480 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
@@ -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();
}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
index 39cb115c8..0efc110f4 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
@@ -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 */
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
index 1812dbdc0..01d49c3d7 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
@@ -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 =
- Channel.builder()
- .setNumber(cursor.getString(POSITION_PHONE_NUMBER))
- .setLabel(cursor.getString(POSITION_PHONE_LABEL))
- .setTechnology(cursor.getInt(POSITION_PHONE_TYPE))
- .build();
- if (TextUtils.isEmpty(channel.number())) {
- channel = null;
+ String number = cursor.getString(POSITION_PHONE_NUMBER);
+ Channel channel = null;
+ if (!TextUtils.isEmpty(number)) {
+ channel =
+ Channel.builder()
+ .setNumber(number)
+ .setLabel(cursor.getString(POSITION_PHONE_LABEL))
+ .setTechnology(cursor.getInt(POSITION_PHONE_TECHNOLOGY))
+ .build();
}
+
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;
}