summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/speeddial/loader
diff options
context:
space:
mode:
authorcalderwoodra <calderwoodra@google.com>2018-04-10 14:45:16 -0700
committerCopybara-Service <copybara-piper@google.com>2018-04-10 15:42:13 -0700
commit2bee0528c1f42b698a606f24da4fa652ceb8d322 (patch)
tree7b31dcd02a359b27f32a7476c3ed94bd262f6d82 /java/com/android/dialer/speeddial/loader
parent2bdb59f0392e54be3dc2c57c32ce126e1e4af7cf (diff)
Wire up SpeedDial fragment with SpeedDialUiItemLoader.
This change is mostly just a migration from a cursor loader and cursor to a listenable future and list of POJOs. Bug: 36841782 Test: tap PiperOrigin-RevId: 192349724 Change-Id: I37140dcc2e5e03bc5745573c0d777e18c4f1a880
Diffstat (limited to 'java/com/android/dialer/speeddial/loader')
-rw-r--r--java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java159
-rw-r--r--java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java222
-rw-r--r--java/com/android/dialer/speeddial/loader/UiItemLoader.java32
-rw-r--r--java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java39
4 files changed, 452 insertions, 0 deletions
diff --git a/java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java b/java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java
new file mode 100644
index 000000000..3381bf8c0
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/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.loader;
+
+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/loader/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java
new file mode 100644
index 000000000..c23b67d45
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java
@@ -0,0 +1,222 @@
+/*
+ * 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()
+ * <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)
+@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<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() {
+ return dialerFutureSerializer.submitAsync(
+ () -> backgroundExecutor.submit(this::doInBackground), backgroundExecutor);
+ }
+
+ @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<>();
+
+ // Get all SpeedDialEntries and mark them to be updated or deleted
+ List<SpeedDialEntry> 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<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
+ speedDialUiItems.add(contact);
+
+ } 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<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/loader/UiItemLoader.java b/java/com/android/dialer/speeddial/loader/UiItemLoader.java
new file mode 100644
index 000000000..4b9a7319f
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/UiItemLoader.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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 com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/** Provides operation for loading {@link SpeedDialUiItem SpeedDialUiItems} */
+public interface UiItemLoader {
+
+ /**
+ * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This
+ * list is composed of starred contacts from {@link
+ * com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper} and suggestions from {@link
+ * android.provider.ContactsContract.Contacts#STREQUENT_PHONE_ONLY}.
+ */
+ ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems();
+}
diff --git a/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java b/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java
new file mode 100644
index 000000000..7d01b4380
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+
+/** Dagger component for the speeddial/loader package. */
+@Subcomponent
+public abstract class UiItemLoaderComponent {
+
+ public abstract SpeedDialUiItemLoader speedDialUiItemLoader();
+
+ public static UiItemLoaderComponent get(Context context) {
+ return ((UiItemLoaderComponent.HasComponent)
+ ((HasRootComponent) context.getApplicationContext()).component())
+ .uiItemLoaderComponent();
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ UiItemLoaderComponent uiItemLoaderComponent();
+ }
+}