From d83c23c9fe6cc4eb2923d8b8b258c5f577b7eef3 Mon Sep 17 00:00:00 2001 From: twyen Date: Wed, 27 Jun 2018 14:48:00 -0700 Subject: Request high resolution photo to be downloaded by the sync adapter when a contact is added to the favorites. To conserve resources synced contacts only have the low-res icon by default, and the hi-res photo is only synced when the contact is viewed. When a contact is "viewed" in dialer, dialer should send a ACTION_VIEW with the contact URI to the sync adapter service. TEST=TAP Test: TAP PiperOrigin-RevId: 202373390 Change-Id: Ie3a173b7c3f442dc806a719910aea9b3a6c5cf4f --- .../android/dialer/contacts/ContactsComponent.java | 5 + .../android/dialer/contacts/ContactsModule.java | 9 +- .../hiresphoto/HighResolutionPhotoRequester.java | 30 +++++ .../HighResolutionPhotoRequesterImpl.java | 137 +++++++++++++++++++++ .../speeddial/loader/SpeedDialUiItemMutator.java | 25 +++- 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequester.java create mode 100644 java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequesterImpl.java diff --git a/java/com/android/dialer/contacts/ContactsComponent.java b/java/com/android/dialer/contacts/ContactsComponent.java index 5c4097ace..9c6773716 100644 --- a/java/com/android/dialer/contacts/ContactsComponent.java +++ b/java/com/android/dialer/contacts/ContactsComponent.java @@ -18,7 +18,9 @@ package com.android.dialer.contacts; import android.content.Context; import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences; +import com.android.dialer.contacts.hiresphoto.HighResolutionPhotoRequester; import com.android.dialer.inject.HasRootComponent; +import com.android.dialer.inject.IncludeInDialerRoot; import dagger.Subcomponent; /** Component for contacts related utilities */ @@ -27,12 +29,15 @@ public abstract class ContactsComponent { public abstract ContactDisplayPreferences contactDisplayPreferences(); + public abstract HighResolutionPhotoRequester highResolutionPhotoLoader(); + public static ContactsComponent get(Context context) { return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) .contactsComponent(); } /** Used to refer to the root application component. */ + @IncludeInDialerRoot public interface HasComponent { ContactsComponent contactsComponent(); } diff --git a/java/com/android/dialer/contacts/ContactsModule.java b/java/com/android/dialer/contacts/ContactsModule.java index 979c525eb..73731e544 100644 --- a/java/com/android/dialer/contacts/ContactsModule.java +++ b/java/com/android/dialer/contacts/ContactsModule.java @@ -18,6 +18,8 @@ package com.android.dialer.contacts; import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences; import com.android.dialer.contacts.displaypreference.ContactDisplayPreferencesImpl; +import com.android.dialer.contacts.hiresphoto.HighResolutionPhotoRequester; +import com.android.dialer.contacts.hiresphoto.HighResolutionPhotoRequesterImpl; import com.android.dialer.inject.DialerVariant; import com.android.dialer.inject.InstallIn; import dagger.Binds; @@ -28,5 +30,10 @@ import dagger.Module; @Module public abstract class ContactsModule { @Binds - public abstract ContactDisplayPreferences to(ContactDisplayPreferencesImpl impl); + public abstract ContactDisplayPreferences toContactDisplayPreferencesImpl( + ContactDisplayPreferencesImpl impl); + + @Binds + public abstract HighResolutionPhotoRequester toHighResolutionPhotoRequesterImpl( + HighResolutionPhotoRequesterImpl impl); } diff --git a/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequester.java b/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequester.java new file mode 100644 index 000000000..1075ec171 --- /dev/null +++ b/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequester.java @@ -0,0 +1,30 @@ +/* + * 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.contacts.hiresphoto; + +import android.net.Uri; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Requests the contacts sync adapter to load a high resolution photo for the contact, typically + * when we will try to show the contact in a larger view (favorites, incall UI, etc.). If a high + * resolution photo is synced, the uri will be notified. + */ +public interface HighResolutionPhotoRequester { + + ListenableFuture request(Uri contactUri); +} diff --git a/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequesterImpl.java b/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequesterImpl.java new file mode 100644 index 000000000..9201604be --- /dev/null +++ b/java/com/android/dialer/contacts/hiresphoto/HighResolutionPhotoRequesterImpl.java @@ -0,0 +1,137 @@ +/* + * 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.contacts.hiresphoto; + +import android.content.ComponentName; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.RawContacts; +import android.support.annotation.VisibleForTesting; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.database.Selection; +import com.android.dialer.inject.ApplicationContext; +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; + +/** Use the contacts sync adapter to load high resolution photos for a Google account. */ +public class HighResolutionPhotoRequesterImpl implements HighResolutionPhotoRequester { + + private static class RequestFailedException extends Exception { + RequestFailedException(String message) { + super(message); + } + + RequestFailedException(String message, Throwable cause) { + super(message, cause); + } + } + + @VisibleForTesting + static final ComponentName SYNC_HIGH_RESOLUTION_PHOTO_SERVICE = + new ComponentName( + "com.google.android.syncadapters.contacts", + "com.google.android.syncadapters.contacts.SyncHighResPhotoIntentService"); + + private final Context appContext; + private final ListeningExecutorService backgroundExecutor; + + @Inject + HighResolutionPhotoRequesterImpl( + @ApplicationContext Context appContext, + @BackgroundExecutor ListeningExecutorService backgroundExecutor) { + this.appContext = appContext; + this.backgroundExecutor = backgroundExecutor; + } + + @Override + public ListenableFuture request(Uri contactUri) { + return backgroundExecutor.submit( + () -> { + try { + requestInternal(contactUri); + } catch (RequestFailedException e) { + LogUtil.e("HighResolutionPhotoRequesterImpl.request", "request failed", e); + } + return null; + }); + } + + private void requestInternal(Uri contactUri) throws RequestFailedException { + for (Long rawContactId : getGoogleRawContactIds(getContactId(contactUri))) { + Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setComponent(SYNC_HIGH_RESOLUTION_PHOTO_SERVICE); + intent.setDataAndType(rawContactUri, RawContacts.CONTENT_ITEM_TYPE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + LogUtil.i( + "HighResolutionPhotoRequesterImpl.requestInternal", + "requesting photo for " + rawContactUri); + appContext.startService(intent); + } catch (IllegalStateException | SecurityException e) { + throw new RequestFailedException("unable to start sync adapter", e); + } + } + } + + private long getContactId(Uri contactUri) throws RequestFailedException { + try (Cursor cursor = + appContext + .getContentResolver() + .query(contactUri, new String[] {Contacts._ID}, null, null, null)) { + if (cursor == null || !cursor.moveToFirst()) { + throw new RequestFailedException("cannot get contact ID"); + } + return cursor.getLong(0); + } + } + + private List getGoogleRawContactIds(long contactId) throws RequestFailedException { + List result = new ArrayList<>(); + Selection selection = + Selection.column(RawContacts.CONTACT_ID) + .is("=", contactId) + .buildUpon() + .and(Selection.column(RawContacts.ACCOUNT_TYPE).is("=", "com.google")) + .build(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + RawContacts.CONTENT_URI, + new String[] {RawContacts._ID, RawContacts.ACCOUNT_TYPE}, + selection.getSelection(), + selection.getSelectionArgs(), + null)) { + if (cursor == null) { + throw new RequestFailedException("null cursor from raw contact IDs"); + } + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + result.add(cursor.getLong(0)); + } + } + return result; + } +} diff --git a/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java index b0b83ac32..86d5d37a9 100644 --- a/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java +++ b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java @@ -34,11 +34,14 @@ import android.util.ArraySet; 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.DefaultFutureCallback; import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; import com.android.dialer.common.concurrent.DialerFutureSerializer; import com.android.dialer.common.database.Selection; +import com.android.dialer.contacts.ContactsComponent; import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences; import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.DisplayOrder; +import com.android.dialer.contacts.hiresphoto.HighResolutionPhotoRequester; import com.android.dialer.duo.DuoComponent; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.speeddial.database.SpeedDialEntry; @@ -49,8 +52,10 @@ import com.android.dialer.util.CallUtil; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -86,15 +91,18 @@ public final class SpeedDialUiItemMutator { // Used to ensure that only one refresh flow runs at a time. private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); private final ContactDisplayPreferences contactDisplayPreferences; + private final HighResolutionPhotoRequester highResolutionPhotoRequester; @Inject public SpeedDialUiItemMutator( @ApplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor, - ContactDisplayPreferences contactDisplayPreferences) { + ContactDisplayPreferences contactDisplayPreferences, + HighResolutionPhotoRequester highResolutionPhotoRequester) { this.appContext = appContext; this.backgroundExecutor = backgroundExecutor; this.contactDisplayPreferences = contactDisplayPreferences; + this.highResolutionPhotoRequester = highResolutionPhotoRequester; } /** @@ -287,6 +295,7 @@ public final class SpeedDialUiItemMutator { Trace.endSection(); // addStarredContact Trace.beginSection("insertUpdateAndDelete"); + requestHighResolutionPhoto(entriesToInsert); ImmutableMap insertedEntriesToIdsMap = db.insertUpdateAndDelete( ImmutableList.copyOf(entriesToInsert), @@ -297,6 +306,20 @@ public final class SpeedDialUiItemMutator { return speedDialUiItemsWithUpdatedIds(speedDialUiItems, insertedEntriesToIdsMap); } + @WorkerThread + private void requestHighResolutionPhoto(List newEntries) { + ContactsComponent.get(appContext).highResolutionPhotoLoader(); + for (SpeedDialEntry entry : newEntries) { + Uri uri; + uri = Contacts.getLookupUri(entry.contactId(), entry.lookupKey()); + + Futures.addCallback( + highResolutionPhotoRequester.request(uri), + new DefaultFutureCallback<>(), + MoreExecutors.directExecutor()); + } + } + /** * Since newly starred contacts sometimes aren't in the SpeedDialEntry database, we couldn't set * their ids when we created our initial list of {@link SpeedDialUiItem speedDialUiItems}. Now -- cgit v1.2.3