From 62b96291dd2bf025f0f84ba0376301a31a6b8776 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Mon, 9 Apr 2018 14:16:47 -0700 Subject: 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 --- .../android/dialer/speeddial/SpeedDialUiItem.java | 159 +++++++++++++++ .../dialer/speeddial/SpeedDialUiItemLoader.java | 225 +++++++++++++++++++++ .../dialer/speeddial/database/SpeedDialEntry.java | 10 +- .../speeddial/database/SpeedDialEntryDao.java | 6 +- .../database/SpeedDialEntryDatabaseHelper.java | 32 +-- 5 files changed, 416 insertions(+), 16 deletions(-) create mode 100644 java/com/android/dialer/speeddial/SpeedDialUiItem.java create mode 100644 java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java (limited to 'java/com/android/dialer/speeddial') 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. + * + *

Contains all data needed for the UI so that the UI never needs do additional contact queries. + * + *

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 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 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 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() + *

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

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; } -- cgit v1.2.3