From 2bee0528c1f42b698a606f24da4fa652ceb8d322 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Tue, 10 Apr 2018 14:45:16 -0700 Subject: 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 --- .../basecomponent/BaseDialerRootComponent.java | 2 + .../dialer/speeddial/FavoritesViewHolder.java | 82 ++++---- .../android/dialer/speeddial/SpeedDialAdapter.java | 118 +++++++---- .../android/dialer/speeddial/SpeedDialCursor.java | 148 -------------- .../dialer/speeddial/SpeedDialFragment.java | 70 +++---- .../android/dialer/speeddial/SpeedDialUiItem.java | 159 --------------- .../dialer/speeddial/SpeedDialUiItemLoader.java | 216 -------------------- .../speeddial/StrequentContactsCursorLoader.java | 97 --------- .../dialer/speeddial/SuggestionViewHolder.java | 61 +++--- .../dialer/speeddial/database/SpeedDialEntry.java | 1 + .../dialer/speeddial/loader/SpeedDialUiItem.java | 159 +++++++++++++++ .../speeddial/loader/SpeedDialUiItemLoader.java | 222 +++++++++++++++++++++ .../dialer/speeddial/loader/UiItemLoader.java | 32 +++ .../speeddial/loader/UiItemLoaderComponent.java | 39 ++++ 14 files changed, 633 insertions(+), 773 deletions(-) delete mode 100644 java/com/android/dialer/speeddial/SpeedDialCursor.java delete mode 100644 java/com/android/dialer/speeddial/SpeedDialUiItem.java delete mode 100644 java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java delete mode 100644 java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java create mode 100644 java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java create mode 100644 java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java create mode 100644 java/com/android/dialer/speeddial/loader/UiItemLoader.java create mode 100644 java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index b668d9114..2d3ef19f8 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -36,6 +36,7 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; +import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; import com.android.incallui.calllocation.CallLocationComponent; @@ -67,6 +68,7 @@ public interface BaseDialerRootComponent PhoneLookupDatabaseComponent.HasComponent, PhoneNumberGeoUtilComponent.HasComponent, PreCallComponent.HasComponent, + UiItemLoaderComponent.HasComponent, SimSuggestionComponent.HasComponent, SimulatorComponent.HasComponent, SpamComponent.HasComponent, diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index c25b05ead..92ffb0a46 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -17,13 +17,8 @@ package com.android.dialer.speeddial; import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -31,8 +26,12 @@ import android.widget.FrameLayout; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.common.Assert; -import com.android.dialer.contactphoto.ContactPhotoManager; -import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; +import com.android.dialer.glidephotomanager.PhotoInfo; +import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.android.dialer.speeddial.loader.SpeedDialUiItem; +import java.util.ArrayList; +import java.util.List; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder @@ -48,7 +47,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private boolean hasDefaultNumber; private boolean isVideoCall; private String number; - private String lookupKey; + private List channels; public FavoritesViewHolder(View view, FavoriteContactsListener listener) { super(view); @@ -62,44 +61,37 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder this.listener = listener; } - public void bind(Context context, Cursor cursor) { - Assert.checkArgument(cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1); - isVideoCall = false; // TODO(calderwoodra): get from disambig data - number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); + public void bind(Context context, SpeedDialUiItem speedDialUiItem) { + Assert.checkArgument(speedDialUiItem.isStarred()); - String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); - long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); - lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY); - Uri contactUri = Contacts.getLookupUri(contactId, lookupKey); + nameView.setText(speedDialUiItem.name()); + hasDefaultNumber = speedDialUiItem.defaultChannel() != null; + if (hasDefaultNumber) { + channels = new ArrayList<>(); + isVideoCall = speedDialUiItem.defaultChannel().isVideoTechnology(); + number = speedDialUiItem.defaultChannel().number(); + phoneType.setText(speedDialUiItem.defaultChannel().label()); + videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); + } else { + channels = speedDialUiItem.channels(); + isVideoCall = false; + number = null; + phoneType.setText(""); + videoCallIcon.setVisibility(View.GONE); + } - String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI); - ContactPhotoManager.getInstance(context) - .loadDialerThumbnailOrPhoto( + GlidePhotoManagerComponent.get(context) + .glidePhotoManager() + .loadQuickContactBadge( photoView, - contactUri, - cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID), - photoUri == null ? null : Uri.parse(photoUri), - name, - LetterTileDrawable.TYPE_DEFAULT); - nameView.setText(name); - phoneType.setText(getLabel(context.getResources(), cursor)); - videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); - - // TODO(calderwoodra): Update this to include communication avenues also - hasDefaultNumber = cursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0; - } - - // TODO(calderwoodra): handle CNAP and cequint types. - // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType - private static String getLabel(Resources resources, Cursor cursor) { - int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE); - String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL); - - // Returns empty label instead of "custom" if the custom label is empty. - if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { - return ""; - } - return (String) Phone.getTypeLabel(resources, numberType, numberLabel); + PhotoInfo.newBuilder() + .setPhotoId(speedDialUiItem.photoId()) + .setPhotoUri(speedDialUiItem.photoUri()) + .setName(speedDialUiItem.name()) + .setLookupUri( + Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey()) + .toString()) + .build()); } @Override @@ -107,7 +99,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder if (hasDefaultNumber) { listener.onClick(number, isVideoCall); } else { - listener.onAmbiguousContactClicked(lookupKey); + listener.onAmbiguousContactClicked(channels); } } @@ -122,7 +114,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact that doesn't have a default number. */ - void onAmbiguousContactClicked(String contactId); + void onAmbiguousContactClicked(List channels); /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java index 5f7b68e5c..3312397c7 100644 --- a/java/com/android/dialer/speeddial/SpeedDialAdapter.java +++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -16,19 +16,30 @@ package com.android.dialer.speeddial; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.LayoutManager; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.util.ArrayMap; import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.common.Assert; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; -import com.android.dialer.speeddial.SpeedDialCursor.RowType; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; +import com.android.dialer.speeddial.loader.SpeedDialUiItem; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * RecyclerView adapter for {@link SpeedDialFragment}. @@ -42,14 +53,26 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen *
  • Suggested contacts * */ -final class SpeedDialAdapter extends RecyclerView.Adapter { +@SuppressWarnings("AndroidApiChecker") +@TargetApi(VERSION_CODES.N) +public final class SpeedDialAdapter extends RecyclerView.Adapter { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION}) + @interface RowType { + int STARRED_HEADER = 0; + int SUGGESTION_HEADER = 1; + int STARRED = 2; + int SUGGESTION = 3; + } private final Context context; private final FavoriteContactsListener favoritesListener; private final SuggestedContactsListener suggestedListener; private final SpeedDialHeaderListener headerListener; - private SpeedDialCursor cursor; + private final Map positionToRowTypeMap = new ArrayMap<>(); + private List speedDialUiItems; public SpeedDialAdapter( Context context, @@ -64,39 +87,45 @@ final class SpeedDialAdapter extends RecyclerView.Adapter immutableSpeedDialUiItems) { + speedDialUiItems = new ArrayList<>(); + speedDialUiItems.addAll(immutableSpeedDialUiItems); + speedDialUiItems.sort((o1, o2) -> Boolean.compare(o2.isStarred(), o1.isStarred())); + positionToRowTypeMap.clear(); + if (speedDialUiItems.isEmpty()) { + return; + } + + // Show the add favorites even if there are no favorite contacts + positionToRowTypeMap.put(0, RowType.STARRED_HEADER); + int positionOfSuggestionHeader = 1; + for (int i = 0; i < speedDialUiItems.size(); i++) { + if (speedDialUiItems.get(i).isStarred()) { + positionToRowTypeMap.put(i + 1, RowType.STARRED); // +1 for the header + positionOfSuggestionHeader++; + } else { + positionToRowTypeMap.put(i + 2, RowType.SUGGESTION); // +2 for both headers + } + } + if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) { + positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER); + } } - LayoutManager getLayoutManager(Context context) { + /* package-private */ LayoutManager getLayoutManager(Context context) { GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); layoutManager.setSpanSizeLookup( new SpanSizeLookup() { @@ -127,15 +176,16 @@ final class SpeedDialAdapter extends RecyclerView.Adapter cursorList = new ArrayList<>(); - if (starred.getCount() > 0) { - cursorList.add(createHeaderCursor(R.string.favorites_header)); - cursorList.add(starred); - } - if (suggestions.getCount() > 0) { - cursorList.add(createHeaderCursor(R.string.suggestions_header)); - cursorList.add(suggestions); - } - return cursorList.toArray(new Cursor[cursorList.size()]); - } - - private static Cursor createHeaderCursor(@StringRes int header) { - MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); - cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); - return cursor; - } - - @RowType - int getRowType(int position) { - moveToPosition(position); - if (getColumnCount() == 1) { - return RowType.HEADER; - } else if (getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { - return RowType.STARRED; - } else { - return RowType.SUGGESTION; - } - } - - @SuppressLint("DefaultLocale") - @StringRes - int getHeader() { - if (getRowType(getPosition()) != RowType.HEADER) { - throw Assert.createIllegalStateFailException( - String.format("Current position (%d) is not a header.", getPosition())); - } - return getInt(HEADER_COLUMN_POSITION); - } - - public boolean hasFavorites() { - return hasFavorites; - } - - private SpeedDialCursor(Cursor[] cursors) { - super(cursors); - for (Cursor cursor : cursors) { - cursor.moveToFirst(); - if (cursor.getColumnCount() != 1 - && cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { - hasFavorites = true; - break; - } - } - } -} diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index 03a3c75bf..d1f195be1 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -17,23 +17,28 @@ package com.android.dialer.speeddial; import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; -import com.android.dialer.common.Assert; import com.android.dialer.precall.PreCall; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; +import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.android.dialer.speeddial.loader.SpeedDialUiItem; +import com.android.dialer.speeddial.loader.UiItemLoaderComponent; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.List; /** * Fragment for displaying: @@ -47,13 +52,9 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen */ public class SpeedDialFragment extends Fragment { - private static final int STREQUENT_CONTACTS_LOADER_ID = 1; - private final SpeedDialHeaderListener headerListener = new SpeedDialFragmentHeaderListener(); private final FavoriteContactsListener favoritesListener = new SpeedDialFavoritesListener(); private final SuggestedContactsListener suggestedListener = new SpeedDialSuggestedListener(); - private final SpeedDialFragmentLoaderCallback loaderCallback = - new SpeedDialFragmentLoaderCallback(); private SpeedDialAdapter adapter; @@ -72,7 +73,6 @@ public class SpeedDialFragment extends Fragment { new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener); recyclerView.setLayoutManager(adapter.getLayoutManager(getContext())); recyclerView.setAdapter(adapter); - getLoaderManager().initLoader(STREQUENT_CONTACTS_LOADER_ID, null /* args */, loaderCallback); return view; } @@ -84,7 +84,28 @@ public class SpeedDialFragment extends Fragment { @Override public void onResume() { super.onResume(); - getLoaderManager().restartLoader(STREQUENT_CONTACTS_LOADER_ID, null, loaderCallback); + Futures.addCallback( + UiItemLoaderComponent.get(getContext().getApplicationContext()) + .speedDialUiItemLoader() + .loadSpeedDialUiItems(), + new FutureCallback>() { + @Override + public void onSuccess(List speedDialUiItems) { + // TODO(calderwoodra): this is bad + new Handler(Looper.getMainLooper()) + .post( + () -> { + adapter.setSpeedDialUiItems(speedDialUiItems); + adapter.notifyDataSetChanged(); + }); + } + + @Override + public void onFailure(Throwable throwable) { + throw new RuntimeException(throwable); + } + }, + MoreExecutors.directExecutor()); } private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener { @@ -98,8 +119,8 @@ public class SpeedDialFragment extends Fragment { private class SpeedDialFavoritesListener implements FavoriteContactsListener { @Override - public void onAmbiguousContactClicked(String lookupKey) { - DisambigDialog.show(lookupKey, getFragmentManager()); + public void onAmbiguousContactClicked(List channels) { + // TODO(calderwoodra): implement the disambig dialog with channels } @Override @@ -130,29 +151,4 @@ public class SpeedDialFragment extends Fragment { getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL)); } } - - /** - * Loader callback that registers a content observer. {@link #unregisterContentObserver()} needs - * to be called during tear down of the fragment. - */ - private class SpeedDialFragmentLoaderCallback implements LoaderCallbacks { - - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id == STREQUENT_CONTACTS_LOADER_ID) { - return new StrequentContactsCursorLoader(getContext()); - } - throw Assert.createIllegalStateFailException("Invalid loader id: " + id); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - adapter.setCursor((SpeedDialCursor) data); - } - - @Override - public void onLoaderReset(Loader loader) { - adapter.setCursor(null); - } - } } diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItem.java b/java/com/android/dialer/speeddial/SpeedDialUiItem.java deleted file mode 100644 index 17552adf7..000000000 --- a/java/com/android/dialer/speeddial/SpeedDialUiItem.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 deleted file mode 100644 index 13e5f8744..000000000 --- a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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<>(); - - // 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(calderwoodra): set the defaults of these automatically - 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 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/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java deleted file mode 100644 index a2dcfdc40..000000000 --- a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; -import android.support.v4.content.CursorLoader; - -/** Cursor Loader for strequent contacts. */ -public final class StrequentContactsCursorLoader extends CursorLoader { - - static final int PHONE_ID = 0; - static final int PHONE_DISPLAY_NAME = 1; - static final int PHONE_STARRED = 2; - static final int PHONE_PHOTO_URI = 3; - static final int PHONE_LOOKUP_KEY = 4; - static final int PHONE_PHOTO_ID = 5; - static final int PHONE_NUMBER = 6; - static final int PHONE_TYPE = 7; - static final int PHONE_LABEL = 8; - static final int PHONE_IS_SUPER_PRIMARY = 9; - static final int PHONE_PINNED = 10; - static final int PHONE_CONTACT_ID = 11; - - public static final String[] PHONE_PROJECTION = - new String[] { - Phone._ID, // 0 - Phone.DISPLAY_NAME, // 1 - Phone.STARRED, // 2 - Phone.PHOTO_URI, // 3 - Phone.LOOKUP_KEY, // 4 - Phone.PHOTO_ID, // 5 - Phone.NUMBER, // 6 - Phone.TYPE, // 7 - Phone.LABEL, // 8 - Phone.IS_SUPER_PRIMARY, // 9 - Phone.PINNED, // 10 - Phone.CONTACT_ID, // 11 - }; - - StrequentContactsCursorLoader(Context context) { - super( - context, - buildUri(), - PHONE_PROJECTION, - null /* selection */, - null /* selectionArgs */, - null /* sortOrder */); - // TODO(calderwoodra): implement alternative display names - } - - static void addToCursor(MatrixCursor dest, Cursor source) { - dest.newRow() - .add(PHONE_PROJECTION[PHONE_ID], source.getLong(PHONE_ID)) - .add(PHONE_PROJECTION[PHONE_DISPLAY_NAME], source.getString(PHONE_DISPLAY_NAME)) - .add(PHONE_PROJECTION[PHONE_STARRED], source.getInt(PHONE_STARRED)) - .add(PHONE_PROJECTION[PHONE_PHOTO_URI], source.getString(PHONE_PHOTO_URI)) - .add(PHONE_PROJECTION[PHONE_LOOKUP_KEY], source.getString(PHONE_LOOKUP_KEY)) - .add(PHONE_PROJECTION[PHONE_NUMBER], source.getString(PHONE_NUMBER)) - .add(PHONE_PROJECTION[PHONE_TYPE], source.getInt(PHONE_TYPE)) - .add(PHONE_PROJECTION[PHONE_LABEL], source.getString(PHONE_LABEL)) - .add(PHONE_PROJECTION[PHONE_IS_SUPER_PRIMARY], source.getInt(PHONE_IS_SUPER_PRIMARY)) - .add(PHONE_PROJECTION[PHONE_PINNED], source.getInt(PHONE_PINNED)) - .add(PHONE_PROJECTION[PHONE_CONTACT_ID], source.getLong(PHONE_CONTACT_ID)); - } - - private static Uri buildUri() { - return Contacts.CONTENT_STREQUENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") - .build(); - } - - @Override - public Cursor loadInBackground() { - return SpeedDialCursor.newInstance(super.loadInBackground()); - } -} diff --git a/java/com/android/dialer/speeddial/SuggestionViewHolder.java b/java/com/android/dialer/speeddial/SuggestionViewHolder.java index 213a54f55..9e4c81de8 100644 --- a/java/com/android/dialer/speeddial/SuggestionViewHolder.java +++ b/java/com/android/dialer/speeddial/SuggestionViewHolder.java @@ -17,10 +17,6 @@ package com.android.dialer.speeddial; import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; @@ -28,10 +24,12 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.QuickContactBadge; import android.widget.TextView; -import com.android.dialer.contactphoto.ContactPhotoManager; -import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.common.Assert; +import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; +import com.android.dialer.glidephotomanager.PhotoInfo; import com.android.dialer.location.GeoUtil; import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.speeddial.loader.SpeedDialUiItem; /** ViewHolder for displaying suggested contacts in {@link SpeedDialFragment}. */ public class SuggestionViewHolder extends RecyclerView.ViewHolder implements OnClickListener { @@ -54,46 +52,35 @@ public class SuggestionViewHolder extends RecyclerView.ViewHolder implements OnC this.listener = listener; } - public void bind(Context context, Cursor cursor) { - number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); - number = PhoneNumberHelper.formatNumber(context, number, GeoUtil.getCurrentCountryIso(context)); + public void bind(Context context, SpeedDialUiItem speedDialUiItem) { + Assert.isNotNull(speedDialUiItem.defaultChannel()); + number = + PhoneNumberHelper.formatNumber( + context, + speedDialUiItem.defaultChannel().number(), + GeoUtil.getCurrentCountryIso(context)); - String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); - String label = getLabel(context.getResources(), cursor); + String label = speedDialUiItem.defaultChannel().label(); String secondaryInfo = TextUtils.isEmpty(label) ? number : context.getString(R.string.call_subject_type_and_number, label, number); - nameOrNumberView.setText(name); + nameOrNumberView.setText(speedDialUiItem.name()); numberView.setText(secondaryInfo); - long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); - String lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY); - Uri contactUri = Contacts.getLookupUri(contactId, lookupKey); - - String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI); - ContactPhotoManager.getInstance(context) - .loadDialerThumbnailOrPhoto( + GlidePhotoManagerComponent.get(context) + .glidePhotoManager() + .loadQuickContactBadge( photoView, - contactUri, - cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID), - photoUri == null ? null : Uri.parse(photoUri), - name, - LetterTileDrawable.TYPE_DEFAULT); - } - - // TODO(calderwoodra): handle CNAP and cequint types. - // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType - private static String getLabel(Resources resources, Cursor cursor) { - int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE); - String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL); - - // Returns empty label instead of "custom" if the custom label is empty. - if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { - return ""; - } - return (String) Phone.getTypeLabel(resources, numberType, numberLabel); + PhotoInfo.newBuilder() + .setPhotoId(speedDialUiItem.photoId()) + .setPhotoUri(speedDialUiItem.photoUri()) + .setName(speedDialUiItem.name()) + .setLookupUri( + Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey()) + .toString()) + .build()); } @Override diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java index 5b54b79c8..13ef4e306 100644 --- a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java @@ -88,6 +88,7 @@ public abstract class SpeedDialEntry { public boolean isVideoTechnology() { return technology() == IMS_VIDEO || technology() == DUO; } + /** * Raw phone number as the user entered it. * 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. + * + *

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

      + *
    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 + 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 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/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> 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(); + } +} -- cgit v1.2.3