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
diff --git a/java/com/android/dialer/contacts/ContactsComponent.java b/java/com/android/dialer/contacts/ContactsComponent.java
index 5c4097a..9c67737 100644
--- a/java/com/android/dialer/contacts/ContactsComponent.java
+++ b/java/com/android/dialer/contacts/ContactsComponent.java
@@ -18,7 +18,9 @@
 
 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 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 979c525..73731e5 100644
--- a/java/com/android/dialer/contacts/ContactsModule.java
+++ b/java/com/android/dialer/contacts/ContactsModule.java
@@ -18,6 +18,8 @@
 
 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 @@
 @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 0000000..1075ec1
--- /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<Void> 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 0000000..9201604
--- /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<Void> 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<Long> getGoogleRawContactIds(long contactId) throws RequestFailedException {
+    List<Long> 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 b0b83ac..86d5d37 100644
--- a/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java
+++ b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemMutator.java
@@ -34,11 +34,14 @@
 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.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 @@
   // 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 @@
     Trace.endSection(); // addStarredContact
 
     Trace.beginSection("insertUpdateAndDelete");
+    requestHighResolutionPhoto(entriesToInsert);
     ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap =
         db.insertUpdateAndDelete(
             ImmutableList.copyOf(entriesToInsert),
@@ -297,6 +306,20 @@
     return speedDialUiItemsWithUpdatedIds(speedDialUiItems, insertedEntriesToIdsMap);
   }
 
+  @WorkerThread
+  private void requestHighResolutionPhoto(List<SpeedDialEntry> 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