Fade photos in contact-details and quick-contacts.

Introduces new helper class, ImageViewDrawableSetter, which remembers
the previously-set drawable, so it can transition from that to a new
one.

Change-Id: Ie6ee6d5ccc376cc9d5b7aa945f2622b173bf09e8
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 33e2ab4..b059ef4 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -40,6 +40,7 @@
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.DateUtils;
+import com.android.contacts.util.ImageViewDrawableSetter;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.util.StructuredPostalUtils;
 import com.android.internal.telephony.ITelephony;
@@ -144,6 +145,8 @@
     private Uri mPrimaryPhoneUri = null;
     private ViewEntryDimensions mViewEntryDimensions;
 
+    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
+
     private Button mQuickFixButton;
     private QuickFix mQuickFix;
     private String mDefaultCountryIso;
@@ -435,8 +438,8 @@
             if (mShowStaticPhoto) {
                 mStaticPhotoContainer.setVisibility(View.VISIBLE);
                 ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById(R.id.photo);
-                OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
-                        mContactData, photoView, false);
+                OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
+                        mContext, mContactData, photoView, false);
                 if (mPhotoTouchOverlay != null) {
                     mPhotoTouchOverlay.setVisibility(View.VISIBLE);
                     mPhotoTouchOverlay.setOnClickListener(listener);
@@ -1489,9 +1492,9 @@
 
             // Set the photo if it should be displayed
             if (viewCache.photoView != null) {
-                OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
-                        mContactData, viewCache.photoView,
-                        !PhoneCapabilityTester.isUsingTwoPanes(mContext));
+                final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
+                OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
+                        mContext, mContactData, viewCache.photoView, expandOnClick);
                 viewCache.enableTouchInterceptor(listener);
             }
 
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 6cd48e3..008136d 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -19,6 +19,7 @@
 import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
 import com.android.contacts.util.PhoneCapabilityTester;
+import com.android.contacts.util.ImageViewDrawableSetter;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -60,6 +61,7 @@
     private TextView mStatusView;
     private ImageView mStatusPhotoView;
     private OnClickListener mPhotoClickListener;
+    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
 
     private Listener mListener;
 
@@ -408,16 +410,15 @@
      * from the outside to fully setup the View
      */
     public void loadData(ContactLoader.Result contactData) {
-        if (contactData == null) {
-            return;
-        }
+        if (contactData == null) return;
 
-        // TODO: Move this into the {@link CarouselTab} class when the updates fragment code is more
-        // finalized
-        mPhotoClickListener = ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView,
-                !PhoneCapabilityTester.isUsingTwoPanes(mContext));
-        ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView,
-                mStatusPhotoView);
+        // TODO: Move this into the {@link CarouselTab} class when the updates
+        // fragment code is more finalized.
+        final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
+        mPhotoClickListener = mPhotoSetter.setupContactPhotoForClick(
+                mContext, contactData, mPhotoView, expandOnClick);
+        ContactDetailDisplayUtils.setSocialSnippet(
+                mContext, contactData, mStatusView, mStatusPhotoView);
     }
 
     /**
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index b603e42..04afb89 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -23,6 +23,7 @@
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.DataKind;
 import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.ImageViewDrawableSetter;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
@@ -112,6 +113,8 @@
     private ImageButton mOpenDetailsPushLayerButton;
     private ViewPager mListPager;
 
+    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
+
     /**
      * Keeps the default action per mimetype. Empty if no default actions are set
      */
@@ -296,13 +299,7 @@
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
                 context.getApplicationContext());
         final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
-        final byte[] photo = data.getPhotoBinaryData();
-        if (photo != null) {
-            photoView.setImageBitmap(BitmapFactory.decodeByteArray(photo, 0, photo.length));
-        } else {
-            photoView.setImageResource(
-                    ContactPhotoManager.getDefaultAvatarResId(true, false));
-        }
+        mPhotoSetter.setupContactPhoto(data, photoView);
 
         for (Entity entity : data.getEntities()) {
             final ContentValues entityValues = entity.getEntityValues();
diff --git a/src/com/android/contacts/util/ImageViewDrawableSetter.java b/src/com/android/contacts/util/ImageViewDrawableSetter.java
new file mode 100644
index 0000000..412d162
--- /dev/null
+++ b/src/com/android/contacts/util/ImageViewDrawableSetter.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2012 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.contacts.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.net.Uri;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageView;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.activities.PhotoSelectionActivity;
+import com.android.contacts.model.EntityDeltaList;
+
+import java.util.Arrays;
+
+/**
+ * Initialized with a target ImageView. When provided with a compressed image
+ * (i.e. a byte[]), it appropriately updates the ImageView's Drawable.
+ */
+public class ImageViewDrawableSetter {
+    private ImageView mTarget;
+    private byte[] mCompressed;
+    private Drawable mPreviousDrawable;
+    private static final String TAG = "ImageViewDrawableSetter";
+
+    public ImageViewDrawableSetter() {
+
+    }
+
+    public ImageViewDrawableSetter(ImageView target) {
+        mTarget = target;
+    }
+
+    public void setupContactPhoto(Result contactData, ImageView photoView) {
+        setTarget(photoView);
+        Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
+    }
+
+    public OnClickListener setupContactPhotoForClick(Context context, Result contactData,
+            ImageView photoView, boolean expandPhotoOnClick) {
+        setTarget(photoView);
+        Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
+        return setupClickListener(context, contactData, bitmap, expandPhotoOnClick);
+    }
+
+    /**
+     * Re-initialize to use new target. As a result, the next time a new image
+     * is set, it will immediately be applied to the target (there will be no
+     * fade transition).
+     */
+    private void setTarget(ImageView target) {
+        if (mTarget != target) {
+            mTarget = target;
+            mCompressed = null;
+            mPreviousDrawable = null;
+        }
+    }
+
+    private Bitmap setCompressedImage(byte[] compressed) {
+        if (mPreviousDrawable == null) {
+            // If we don't already have a drawable, skip the exit-early test
+            // below; otherwise we might not end up setting the default image.
+        } else if (mPreviousDrawable != null && Arrays.equals(mCompressed, compressed)) {
+            // TODO: the worst case is when the arrays are equal but not
+            // identical. This takes about 1ms (more with high-res photos). A
+            // possible optimization is to sparsely sample chunks of the arrays
+            // to compare.
+            return previousBitmap();
+        }
+
+        final Drawable newDrawable = (compressed == null)
+                ? defaultDrawable()
+                : decodedBitmapDrawable(compressed);
+
+        // Remember this for next time, so that we can check if it changed.
+        mCompressed = compressed;
+
+        // If we don't have a new Drawable, something went wrong... bail out.
+        if (newDrawable == null) return previousBitmap();
+
+        if (mPreviousDrawable == null) {
+            // Set the new one immediately.
+            mTarget.setImageDrawable(newDrawable);
+        } else {
+            // Set up a transition from the previous Drawable to the new one.
+            final Drawable[] beforeAndAfter = new Drawable[2];
+            beforeAndAfter[0] = mPreviousDrawable;
+            beforeAndAfter[1] = newDrawable;
+            final TransitionDrawable transition = new TransitionDrawable(beforeAndAfter);
+            mTarget.setImageDrawable(transition);
+            transition.startTransition(200);
+        }
+
+        // Remember this for next time, so that we can transition from it to the
+        // new one.
+        mPreviousDrawable = newDrawable;
+
+        return previousBitmap();
+    }
+
+    private Bitmap previousBitmap() {
+        return (mPreviousDrawable == null)
+                ? null
+                : ((BitmapDrawable) mPreviousDrawable).getBitmap();
+    }
+
+    private static final class PhotoClickListener implements OnClickListener {
+
+        private final Context mContext;
+        private final Result mContactData;
+        private final Bitmap mPhotoBitmap;
+        private final byte[] mPhotoBytes;
+        private final boolean mExpandPhotoOnClick;
+        public PhotoClickListener(Context context, Result contactData, Bitmap photoBitmap,
+                byte[] photoBytes, boolean expandPhotoOnClick) {
+            mContext = context;
+            mContactData = contactData;
+            mPhotoBitmap = photoBitmap;
+            mPhotoBytes = photoBytes;
+            mExpandPhotoOnClick = expandPhotoOnClick;
+        }
+
+        @Override
+        public void onClick(View v) {
+            // Assemble the intent.
+            EntityDeltaList delta = EntityDeltaList.fromIterator(
+                    mContactData.getEntities().iterator());
+
+            // Find location and bounds of target view, adjusting based on the
+            // assumed local density.
+            final float appScale =
+                    mContext.getResources().getCompatibilityInfo().applicationScale;
+            final int[] pos = new int[2];
+            v.getLocationOnScreen(pos);
+
+            final Rect rect = new Rect();
+            rect.left = (int) (pos[0] * appScale + 0.5f);
+            rect.top = (int) (pos[1] * appScale + 0.5f);
+            rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
+            rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
+
+            Uri photoUri = null;
+            if (mContactData.getPhotoUri() != null) {
+                photoUri = Uri.parse(mContactData.getPhotoUri());
+            }
+            Intent photoSelectionIntent = PhotoSelectionActivity.buildIntent(mContext,
+                    photoUri, mPhotoBitmap, mPhotoBytes, rect, delta, mContactData.isUserProfile(),
+                    mContactData.isDirectoryEntry(), mExpandPhotoOnClick);
+            // Cache the bitmap directly, so the activity can pull it from the photo manager.
+            if (mPhotoBitmap != null) {
+                ContactPhotoManager.getInstance(mContext).cacheBitmap(
+                        photoUri, mPhotoBitmap, mPhotoBytes);
+            }
+            mContext.startActivity(photoSelectionIntent);
+        }
+    }
+
+    private OnClickListener setupClickListener(Context context, Result contactData, Bitmap bitmap,
+            boolean expandPhotoOnClick) {
+        if (mTarget == null) return null;
+
+        OnClickListener clickListener = new PhotoClickListener(
+                context, contactData, bitmap, mCompressed, expandPhotoOnClick);
+        mTarget.setOnClickListener(clickListener);
+        return clickListener;
+    }
+
+    /**
+     * Obtain the default drawable for a contact when no photo is available.
+     */
+    private Drawable defaultDrawable() {
+        Resources resources = mTarget.getResources();
+        final int resId = ContactPhotoManager.getDefaultAvatarResId(true, false);
+        try {
+            return resources.getDrawable(resId);
+        } catch (NotFoundException e) {
+            Log.wtf(TAG, "Cannot load default avatar resource.");
+            return null;
+        }
+    }
+
+    private BitmapDrawable decodedBitmapDrawable(byte[] compressed) {
+        Resources rsrc = mTarget.getResources();
+        Bitmap bitmap = BitmapFactory.decodeByteArray(compressed, 0, compressed.length);
+        return new BitmapDrawable(rsrc, bitmap);
+    }
+
+}