Add contact photo for missed call notifications

+ ContactPhotoLoader to create the appropriate icon from a ContactInfo
- NameLookupQuery in CallLogNotificationsHelper#getContactInfo
  To show a photo the name is not enough. Full query need to be made to
  retrieve the photoUri.
+ class Assert in util
+ Gradle directory setup for dialer tests
  (Note: this is just for project setup in Android Studio, tests are
  still not runnable in gradle)

Bug:27276108
Change-Id: I0ed2147f2bb60454fe5a5ad6c25fe99727441880
diff --git a/build-app.gradle b/build-app.gradle
index 9e24136..2ea4376 100644
--- a/build-app.gradle
+++ b/build-app.gradle
@@ -12,6 +12,11 @@
         manifest.srcFile 'AndroidManifest.xml'
         res.srcDirs = ['res']
     }
+
+    sourceSets.androidTest {
+        java.srcDirs = ['tests/src']
+        res.srcDirs = ['test/res']
+    }
 }
 
 dependencies {
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index e775b0a..025d3eb 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -16,9 +16,6 @@
 
 package com.android.dialer;
 
-import com.android.dialer.voicemail.VoicemailArchiveActivity;
-import com.google.common.annotations.VisibleForTesting;
-
 import android.app.Fragment;
 import android.app.FragmentTransaction;
 import android.content.ActivityNotFoundException;
@@ -65,7 +62,6 @@
 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.widget.FloatingActionButtonController;
-import com.android.contacts.commonbind.analytics.AnalyticsUtil;
 import com.android.dialer.calllog.CallLogActivity;
 import com.android.dialer.calllog.CallLogFragment;
 import com.android.dialer.database.DialerDatabaseHelper;
@@ -85,18 +81,19 @@
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.ScreenEvent;
 import com.android.dialer.settings.DialerSettingsActivity;
+import com.android.dialer.util.Assert;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.dialer.util.TelecomUtil;
+import com.android.dialer.voicemail.VoicemailArchiveActivity;
 import com.android.dialer.widget.ActionBarController;
 import com.android.dialer.widget.SearchEditTextLayout;
 import com.android.dialerbind.DatabaseHelperManager;
 import com.android.dialerbind.ObjectFactory;
 import com.android.phone.common.animation.AnimUtils;
 import com.android.phone.common.animation.AnimationListenerAdapter;
-
-import junit.framework.Assert;
+import com.google.common.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
index 6abf241..1892631 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
@@ -16,9 +16,7 @@
 
 package com.android.dialer.calllog;
 
-import static android.Manifest.permission.READ_CALL_LOG;
-import static android.Manifest.permission.READ_CONTACTS;
-
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
@@ -111,7 +109,7 @@
     /**
      * Given a number and number information (presentation and country ISO), get
      * {@link ContactInfo}. If the name is empty but we have a special presentation, display that.
-     * Otherwise attempt to look it up in the database or the cache.
+     * Otherwise attempt to look it up in the cache.
      * If that fails, fall back to displaying the number.
      */
     public @NonNull ContactInfo getContactInfo(@Nullable String number, int numberPresentation,
@@ -136,13 +134,7 @@
             return contactInfo;
         }
 
-        // 2. Personal ContactsProvider phonelookup query.
-        contactInfo.name = mNameLookupQuery.query(number);
-        if (!TextUtils.isEmpty(contactInfo.name)) {
-            return contactInfo;
-        }
-
-        // 3. Look it up in the cache.
+        // 2. Look it up in the cache.
         ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
 
         if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
@@ -150,13 +142,13 @@
         }
 
         if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
-            // 4. If we cannot lookup the contact, use the formatted number instead.
+            // 3. If we cannot lookup the contact, use the formatted number instead.
             contactInfo.name = contactInfo.formattedNumber;
         } else if (!TextUtils.isEmpty(number)) {
-            // 5. If number can't be formatted, use number.
+            // 4. If number can't be formatted, use number.
             contactInfo.name = number;
         } else {
-            // 6. Otherwise, it's unknown number.
+            // 5. Otherwise, it's unknown number.
             contactInfo.name = mContext.getResources().getString(R.string.unknown);
         }
         return contactInfo;
@@ -259,7 +251,7 @@
         @Override
         @Nullable
         public List<NewCall> query(int type) {
-            if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
+            if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
                 Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
                 return null;
             }
@@ -338,7 +330,7 @@
         @Override
         @Nullable
         public String query(@Nullable String number) {
-            if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
+            if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CONTACTS)) {
                 Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
                 return null;
             }
diff --git a/src/com/android/dialer/calllog/MissedCallNotifier.java b/src/com/android/dialer/calllog/MissedCallNotifier.java
index a9dfd44..c422dd5 100644
--- a/src/com/android/dialer/calllog/MissedCallNotifier.java
+++ b/src/com/android/dialer/calllog/MissedCallNotifier.java
@@ -21,6 +21,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.os.AsyncTask;
 import android.provider.CallLog.Calls;
 import android.text.TextUtils;
@@ -28,13 +29,14 @@
 
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.util.PhoneNumberHelper;
-import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
 import com.android.dialer.DialtactsActivity;
+import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.contactinfo.ContactPhotoLoader;
 import com.android.dialer.list.ListsFragment;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.IntentUtil.CallIntentBuilder;
-import com.android.dialer.R;
 
 import java.util.List;
 
@@ -94,6 +96,7 @@
         NewCall newestCall = useCallLog ? newCalls.get(0) : null;
         long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
 
+        Notification.Builder builder = new Notification.Builder(mContext);
         // Display the first line of the notification:
         // 1 missed call: <caller name || handle>
         // More than 1 missed call: <number of calls> + "missed calls"
@@ -110,6 +113,11 @@
                     : R.string.notification_missedCallTitle;
 
             expandedText = contactInfo.name;
+            ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
+            Bitmap photoIcon = loader.loadPhotoIcon();
+            if (photoIcon != null) {
+                builder.setLargeIcon(photoIcon);
+            }
         } else {
             titleResId = R.string.notification_missedCallsTitle;
             expandedText =
@@ -132,7 +140,6 @@
                 .setDeleteIntent(createClearMissedCallsPendingIntent());
 
         // Create the notification suitable for display when sensitive information is showing.
-        Notification.Builder builder = new Notification.Builder(mContext);
         builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
                 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
                 .setContentTitle(mContext.getText(titleResId))
@@ -161,7 +168,6 @@
                             createSendSmsFromNotificationPendingIntent(number));
                 }
             }
-            //TODO: add photo
         }
 
         Notification notification = builder.build();
diff --git a/src/com/android/dialer/contactinfo/ContactPhotoLoader.java b/src/com/android/dialer/contactinfo/ContactPhotoLoader.java
new file mode 100644
index 0000000..f36c438
--- /dev/null
+++ b/src/com/android/dialer/contactinfo/ContactPhotoLoader.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 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.contactinfo;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.provider.MediaStore;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.R;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.util.Assert;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.io.IOException;
+/**
+ * Class to create the appropriate contact icon from a ContactInfo.
+ * This class is for synchronous, blocking calls to generate bitmaps, while
+ * ContactCommons.ContactPhotoManager is to cache, manage and update a ImageView asynchronously.
+ */
+public class ContactPhotoLoader {
+
+    private static final String TAG = "ContactPhotoLoader";
+
+    private final Context mContext;
+    private final ContactInfo mContactInfo;
+
+    public ContactPhotoLoader(Context context, ContactInfo contactInfo) {
+        mContext = Preconditions.checkNotNull(context);
+        mContactInfo = Preconditions.checkNotNull(contactInfo);
+    }
+
+    /**
+     * Create a contact photo icon bitmap appropriate for the ContactInfo.
+     */
+    public Bitmap loadPhotoIcon() {
+        Assert.assertNotUiThread("ContactPhotoLoader#loadPhotoIcon called on UI thread");
+        int photoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+        return drawableToBitmap(getIcon(), photoSize, photoSize);
+    }
+
+    @VisibleForTesting
+    Drawable getIcon() {
+        Drawable drawable = createPhotoIconDrawable();
+        if (drawable == null) {
+            drawable = createLetterTileDrawable();
+        }
+        return drawable;
+    }
+
+    /**
+     * @return a {@link Drawable} of  circular photo icon if the photo can be loaded, {@code null}
+     * otherwise.
+     */
+    @Nullable
+    private Drawable createPhotoIconDrawable() {
+        if (mContactInfo.photoUri == null) {
+            return null;
+        }
+        try {
+            Bitmap bitmap = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(),
+                    mContactInfo.photoUri);
+            final RoundedBitmapDrawable drawable =
+                    RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap);
+            drawable.setAntiAlias(true);
+            drawable.setCornerRadius(bitmap.getHeight() / 2);
+            return drawable;
+        } catch (IOException e) {
+            Log.e(TAG, e.toString());
+            return null;
+        }
+    }
+
+    /**
+     * @return a {@link LetterTileDrawable} based on the ContactInfo.
+     */
+    private Drawable createLetterTileDrawable() {
+        LetterTileDrawable drawable = new LetterTileDrawable(mContext.getResources());
+        drawable.setIsCircular(true);
+        ContactInfoHelper helper =
+                new ContactInfoHelper(mContext, GeoUtil.getCurrentCountryIso(mContext));
+        if (helper.isBusiness(mContactInfo.sourceType)) {
+            drawable.setContactType(LetterTileDrawable.TYPE_BUSINESS);
+        }
+        drawable.setLetterAndColorFromContactDetails(mContactInfo.name, mContactInfo.lookupKey);
+        return drawable;
+    }
+
+    private static Bitmap drawableToBitmap(Drawable drawable, int width, int height) {
+        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+}
diff --git a/src/com/android/dialer/util/Assert.java b/src/com/android/dialer/util/Assert.java
new file mode 100644
index 0000000..ec0a6cc
--- /dev/null
+++ b/src/com/android/dialer/util/Assert.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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.util;
+
+import android.os.Looper;
+
+public class Assert {
+    public static void assertNotUiThread(String msg) {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            throw new AssertionError(msg);
+        }
+    }
+
+    public static void assertNotNull(Object object, String msg) {
+        if (object == null) {
+            throw new AssertionError(object);
+        }
+    }
+
+    public static void assertNotNull(Object object) {
+        assertNotNull(object, null);
+    }
+}
diff --git a/tests/src/com/android/dialer/contactinfo/ContactPhotoLoaderTest.java b/tests/src/com/android/dialer/contactinfo/ContactPhotoLoaderTest.java
new file mode 100644
index 0000000..42a5ae9
--- /dev/null
+++ b/tests/src/com/android/dialer/contactinfo/ContactPhotoLoaderTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 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.contactinfo;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.test.AndroidTestCase;
+import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.tests.R;
+
+public class ContactPhotoLoaderTest extends InstrumentationTestCase {
+
+    private Context mContext;
+
+    @Override
+    public void setUp() {
+        mContext = getInstrumentation().getTargetContext();
+    }
+
+    public void testConstructor() {
+        ContactPhotoLoader loader = new ContactPhotoLoader(mContext, new ContactInfo());
+    }
+
+    public void testConstructor_NullContext() {
+        try {
+            ContactPhotoLoader loader = new ContactPhotoLoader(null, new ContactInfo());
+            fail();
+        } catch (NullPointerException e) {
+            //expected
+        }
+    }
+
+    public void testConstructor_NullContactInfo() {
+        try {
+            ContactPhotoLoader loader = new ContactPhotoLoader(mContext, null);
+            fail();
+        } catch (NullPointerException e) {
+            //expected
+        }
+    }
+
+    public void testGetIcon_Photo() {
+        ContactInfo info = getTestContactInfo();
+        info.photoUri = getResourceUri(R.drawable.phone_icon);
+        ContactPhotoLoader loader = new ContactPhotoLoader(mContext, info);
+        assertTrue(loader.getIcon() instanceof RoundedBitmapDrawable);
+    }
+
+    public void testGetIcon_Photo_Invalid() {
+        ContactInfo info = getTestContactInfo();
+        info.photoUri = Uri.parse("file://invalid/uri");
+        ContactPhotoLoader loader = new ContactPhotoLoader(mContext, info);
+        //Should fall back to LetterTileDrawable
+        assertTrue(loader.getIcon() instanceof LetterTileDrawable);
+    }
+
+    public void testGetIcon_LetterTile() {
+        ContactInfo info = getTestContactInfo();
+        ContactPhotoLoader loader = new ContactPhotoLoader(mContext, info);
+        assertTrue(loader.getIcon() instanceof LetterTileDrawable);
+    }
+
+    private Uri getResourceUri(int resId) {
+        Context testContext = getInstrumentation().getContext();
+        Resources resources = testContext.getResources();
+
+        assertNotNull(resources.getDrawable(resId));
+        return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+                + testContext.getPackageName()
+                + '/' + resId);
+    }
+
+    private ContactInfo getTestContactInfo() {
+        ContactInfo info = new ContactInfo();
+        info.name = "foo";
+        info.lookupKey = "bar";
+        return info;
+    }
+}
\ No newline at end of file