summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorke Lee <yorkelee@google.com>2013-08-07 16:37:46 -0700
committerYorke Lee <yorkelee@google.com>2013-08-07 17:53:24 -0700
commit6d231f52e4f00399330d772f2a337283803f3a9d (patch)
treee99add16999683d1ec62634f71ffca58cad6ffae
parentc171e8108487ccf3663bb8ff4b40a52ca3caa8db (diff)
Add image to caller info
* Add various image setting method sto CallCardFragment * Added AnimationUtils and ContactsAsyncHelper, copied unchanged from Phone Change-Id: I6175ccc2433a5d0ad8a9bffac678a263ee65622c
-rw-r--r--InCallUI/res/values/ids.xml19
-rw-r--r--InCallUI/src/com/android/incallui/AnimationUtils.java279
-rw-r--r--InCallUI/src/com/android/incallui/CallCardFragment.java31
-rw-r--r--InCallUI/src/com/android/incallui/CallCardPresenter.java110
-rw-r--r--InCallUI/src/com/android/incallui/ContactsAsyncHelper.java337
5 files changed, 769 insertions, 7 deletions
diff --git a/InCallUI/res/values/ids.xml b/InCallUI/res/values/ids.xml
new file mode 100644
index 000000000..c6ad2099c
--- /dev/null
+++ b/InCallUI/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<resources>
+ <item type="id" name="fadeState" />
+</resources>
diff --git a/InCallUI/src/com/android/incallui/AnimationUtils.java b/InCallUI/src/com/android/incallui/AnimationUtils.java
new file mode 100644
index 000000000..2bf730cca
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/AnimationUtils.java
@@ -0,0 +1,279 @@
+/*
+ * 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.incallui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.ImageView;
+
+/**
+ * Utilities for Animation.
+ */
+public class AnimationUtils {
+ private static final String LOG_TAG = AnimationUtils.class.getSimpleName();
+ /**
+ * Turn on when you're interested in fading animation. Intentionally untied from other debug
+ * settings.
+ */
+ private static final boolean FADE_DBG = false;
+
+ /**
+ * Duration for animations in msec, which can be used with
+ * {@link ViewPropertyAnimator#setDuration(long)} for example.
+ */
+ public static final int ANIMATION_DURATION = 250;
+
+ private AnimationUtils() {
+ }
+
+ /**
+ * Simple Utility class that runs fading animations on specified views.
+ */
+ public static class Fade {
+
+ // View tag that's set during the fade-out animation; see hide() and
+ // isFadingOut().
+ private static final int FADE_STATE_KEY = R.id.fadeState;
+ private static final String FADING_OUT = "fading_out";
+
+ /**
+ * Sets the visibility of the specified view to View.VISIBLE and then
+ * fades it in. If the view is already visible (and not in the middle
+ * of a fade-out animation), this method will return without doing
+ * anything.
+ *
+ * @param view The view to be faded in
+ */
+ public static void show(final View view) {
+ if (FADE_DBG) log("Fade: SHOW view " + view + "...");
+ if (FADE_DBG) log("Fade: - visibility = " + view.getVisibility());
+ if ((view.getVisibility() != View.VISIBLE) || isFadingOut(view)) {
+ view.animate().cancel();
+ // ...and clear the FADE_STATE_KEY tag in case we just
+ // canceled an in-progress fade-out animation.
+ view.setTag(FADE_STATE_KEY, null);
+
+ view.setAlpha(0);
+ view.setVisibility(View.VISIBLE);
+ view.animate().setDuration(ANIMATION_DURATION);
+ view.animate().alpha(1);
+ if (FADE_DBG) log("Fade: ==> SHOW " + view
+ + " DONE. Set visibility = " + View.VISIBLE);
+ } else {
+ if (FADE_DBG) log("Fade: ==> Ignoring, already visible AND not fading out.");
+ }
+ }
+
+ /**
+ * Fades out the specified view and then sets its visibility to the
+ * specified value (either View.INVISIBLE or View.GONE). If the view
+ * is not currently visibile, the method will return without doing
+ * anything.
+ *
+ * Note that *during* the fade-out the view itself will still have
+ * visibility View.VISIBLE, although the isFadingOut() method will
+ * return true (in case the UI code needs to detect this state.)
+ *
+ * @param view The view to be hidden
+ * @param visibility The value to which the view's visibility will be
+ * set after it fades out.
+ * Must be either View.INVISIBLE or View.GONE.
+ */
+ public static void hide(final View view, final int visibility) {
+ if (FADE_DBG) log("Fade: HIDE view " + view + "...");
+ if (view.getVisibility() == View.VISIBLE &&
+ (visibility == View.INVISIBLE || visibility == View.GONE)) {
+
+ // Use a view tag to mark this view as being in the middle
+ // of a fade-out animation.
+ view.setTag(FADE_STATE_KEY, FADING_OUT);
+
+ view.animate().cancel();
+ view.animate().setDuration(ANIMATION_DURATION);
+ view.animate().alpha(0f).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1);
+ view.setVisibility(visibility);
+ view.animate().setListener(null);
+ // ...and we're done with the fade-out, so clear the view tag.
+ view.setTag(FADE_STATE_KEY, null);
+ if (FADE_DBG) log("Fade: HIDE " + view
+ + " DONE. Set visibility = " + visibility);
+ }
+ });
+ }
+ }
+
+ /**
+ * @return true if the specified view is currently in the middle
+ * of a fade-out animation. (During the fade-out, the view's
+ * visibility is still VISIBLE, although in many cases the UI
+ * should behave as if it's already invisible or gone. This
+ * method allows the UI code to detect that state.)
+ *
+ * @see #hide(View, int)
+ */
+ public static boolean isFadingOut(final View view) {
+ if (FADE_DBG) {
+ log("Fade: isFadingOut view " + view + "...");
+ log("Fade: - getTag() returns: " + view.getTag(FADE_STATE_KEY));
+ log("Fade: - returning: " + (view.getTag(FADE_STATE_KEY) == FADING_OUT));
+ }
+ return (view.getTag(FADE_STATE_KEY) == FADING_OUT);
+ }
+
+ }
+
+ /**
+ * Drawable achieving cross-fade, just like TransitionDrawable. We can have
+ * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}).
+ */
+ private static class CrossFadeDrawable extends LayerDrawable {
+ private final ObjectAnimator mAnimator;
+
+ public CrossFadeDrawable(Drawable[] layers) {
+ super(layers);
+ mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0);
+ }
+
+ private int mCrossFadeAlpha;
+
+ /**
+ * This will be used from ObjectAnimator.
+ * Note: this method is protected by proguard.flags so that it won't be removed
+ * automatically.
+ */
+ @SuppressWarnings("unused")
+ public void setCrossFadeAlpha(int alpha) {
+ mCrossFadeAlpha = alpha;
+ invalidateSelf();
+ }
+
+ public ObjectAnimator getAnimator() {
+ return mAnimator;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ Drawable first = getDrawable(0);
+ Drawable second = getDrawable(1);
+
+ if (mCrossFadeAlpha > 0) {
+ first.setAlpha(mCrossFadeAlpha);
+ first.draw(canvas);
+ first.setAlpha(255);
+ }
+
+ if (mCrossFadeAlpha < 0xff) {
+ second.setAlpha(0xff - mCrossFadeAlpha);
+ second.draw(canvas);
+ second.setAlpha(0xff);
+ }
+ }
+ }
+
+ private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) {
+ Drawable[] layers = new Drawable[2];
+ layers[0] = first;
+ layers[1] = second;
+ return new CrossFadeDrawable(layers);
+ }
+
+ /**
+ * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to"
+ * are the same.
+ */
+ public static void startCrossFade(
+ final ImageView imageView, final Drawable from, final Drawable to) {
+ // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables
+ // pointing to the same Bitmap.
+ final boolean areSameImage = from.equals(to) ||
+ ((from instanceof BitmapDrawable)
+ && (to instanceof BitmapDrawable)
+ && ((BitmapDrawable) from).getBitmap()
+ .equals(((BitmapDrawable) to).getBitmap()));
+ if (!areSameImage) {
+ if (FADE_DBG) {
+ log("Start cross-fade animation for " + imageView
+ + "(" + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+
+ CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to);
+ ObjectAnimator animator = crossFadeDrawable.getAnimator();
+ imageView.setImageDrawable(crossFadeDrawable);
+ animator.setDuration(ANIMATION_DURATION);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (FADE_DBG) {
+ log("cross-fade animation start ("
+ + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (FADE_DBG) {
+ log("cross-fade animation ended ("
+ + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+ animation.removeAllListeners();
+ // Workaround for issue 6300562; this will force the drawable to the
+ // resultant one regardless of animation glitch.
+ imageView.setImageDrawable(to);
+ }
+ });
+ animator.start();
+
+ /* We could use TransitionDrawable here, but it may cause some weird animation in
+ * some corner cases. See issue 6300562
+ * TODO: decide which to be used in the long run. TransitionDrawable is old but system
+ * one. Ours uses new animation framework and thus have callback (great for testing),
+ * while no framework support for the exact class.
+
+ Drawable[] layers = new Drawable[2];
+ layers[0] = from;
+ layers[1] = to;
+ TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(ANIMATION_DURATION); */
+ imageView.setTag(to);
+ } else {
+ if (FADE_DBG) {
+ log("*Not* start cross-fade. " + imageView);
+ }
+ }
+ }
+
+ // Debugging / testing code
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+} \ No newline at end of file
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index 9819f15f7..b584d5e86 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -17,12 +17,16 @@
package com.android.incallui;
import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
+import android.widget.ImageView;
import android.widget.TextView;
/**
@@ -34,6 +38,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter>
private TextView mPhoneNumber;
private TextView mNumberLabel;
private TextView mName;
+ private ImageView mPhoto;
private ViewStub mSecondaryCallInfo;
@@ -56,6 +61,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter>
mName = (TextView) view.findViewById(R.id.name);
mNumberLabel = (TextView) view.findViewById(R.id.label);
mSecondaryCallInfo = (ViewStub) view.findViewById(R.id.secondary_call_info);
+ mPhoto = (ImageView) view.findViewById(R.id.photo);
// This method call will begin the callbacks on CallCardUi. We need to ensure
// everything needed for the callbacks is set up before this is called.
@@ -137,4 +143,29 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter>
}
}
+ @Override
+ public void setImage(int resource) {
+ setImage(getActivity().getResources().getDrawable(resource));
+ }
+
+ @Override
+ public void setImage(Drawable drawable) {
+ setDrawableToImageView(mPhoto, drawable);
+ }
+
+ @Override
+ public void setImage(Bitmap bitmap) {
+ setImage(new BitmapDrawable(getActivity().getResources(), bitmap));
+ }
+
+ private void setDrawableToImageView(ImageView view, Drawable drawable) {
+ final Drawable current = view.getDrawable();
+ if (current == null) {
+ view.setImageDrawable(drawable);
+ AnimationUtils.Fade.show(view);
+ } else {
+ AnimationUtils.startCrossFade(view, current, drawable);
+ mPhoto.setVisibility(View.VISIBLE);
+ }
+ }
}
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index f37956569..06b115763 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -18,6 +18,8 @@ package com.android.incallui;
import android.content.ContentUris;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
@@ -31,11 +33,27 @@ import com.android.services.telephony.common.Call;
* Presenter for the Call Card Fragment.
* This class listens for changes to InCallState and passes it along to the fragment.
*/
-public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
- implements InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener {
+public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> implements
+ InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
+ ContactsAsyncHelper.OnImageLoadCompleteListener {
+
+ private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
private Context mContext;
+ /**
+ * Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded,
+ * or a photo is already loaded.
+ */
+ private Uri mLoadingPersonUri;
+
+ // Track the state for the photo.
+ private ContactsAsyncHelper.ImageTracker mPhotoTracker;
+
+ public CallCardPresenter() {
+ mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
+ }
+
@Override
public void onUiReady(CallCardUi ui) {
super.onUiReady(ui);
@@ -87,6 +105,9 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
void setNumberLabel(String label);
void setName(String name);
void setName(String name, boolean isNumber);
+ void setImage(int resource);
+ void setImage(Drawable drawable);
+ void setImage(Bitmap bitmap);
void setSecondaryCallInfo(boolean show, String number);
}
@@ -116,11 +137,9 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation,
boolean isPrimary) {
- //Todo (klp): Either enable or get rid of this
- // inform the state machine that we are displaying a photo.
- //mPhotoTracker.setPhotoRequest(info);
- //mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
-
+ // Inform the state machine that we are displaying a photo.
+ mPhotoTracker.setPhotoRequest(info);
+ mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
// The actual strings we're going to display onscreen:
String displayName;
@@ -235,10 +254,87 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
updateInfoUiForPrimary(displayName, displayNumber, label);
}
+ // If the photoResource is filled in for the CallerInfo, (like with the
+ // Emergency Number case), then we can just set the photo image without
+ // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
+ // for cases where CallerInfo.photoResource may be set. We can also avoid
+ // the image load step if the image data is cached.
+ final CallCardUi ui = getUi();
+ if (info == null) return;
+
+ // This will only be true for emergency numbers
+ if (info.photoResource != 0) {
+ ui.setImage(info.photoResource);
+ } else if (info.isCachedPhotoCurrent) {
+ if (info.cachedPhoto != null) {
+ ui.setImage(info.cachedPhoto);
+ } else {
+ ui.setImage(R.drawable.picture_unknown);
+ }
+ } else {
+ if (personUri == null) {
+ Logger.v(this, "personUri is null. Just use unknown picture.");
+ ui.setImage(R.drawable.picture_unknown);
+ } else if (personUri.equals(mLoadingPersonUri)) {
+ Logger.v(this, "The requested Uri (" + personUri + ") is being loaded already."
+ + " Ignore the duplicate load request.");
+ } else {
+ // Remember which person's photo is being loaded right now so that we won't issue
+ // unnecessary load request multiple times, which will mess up animation around
+ // the contact photo.
+ mLoadingPersonUri = personUri;
+
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext, personUri, this, call);
+
+ // If the image load is too slow, we show a default avatar icon afterward.
+ // If it is fast enough, this message will be canceled on onImageLoadComplete().
+ // TODO (klp): Figure out if this handler is still needed.
+ // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+ // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
+ }
+ }
// TODO (klp): Update other fields - photo, sip label, etc.
}
/**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
+ * make sure that the call state is reflected after the image is loaded.
+ */
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+ if (mLoadingPersonUri != null) {
+ // Start sending view notification after the current request being done.
+ // New image may possibly be available from the next phone calls.
+ //
+ // TODO: may be nice to update the image view again once the newer one
+ // is available on contacts database.
+ // TODO (klp): What is this, and why does it need the write_contacts permission?
+ // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri);
+ } else {
+ // This should not happen while we need some verbose info if it happens..
+ Logger.v(this, "Person Uri isn't available while Image is successfully loaded.");
+ }
+ mLoadingPersonUri = null;
+
+ Call call = (Call) cookie;
+
+ // TODO (klp): Handle conference calls
+
+ final CallCardUi ui = getUi();
+ if (photo != null) {
+ ui.setImage(photo);
+ } else if (photoIcon != null) {
+ ui.setImage(photoIcon);
+ } else {
+ ui.setImage(R.drawable.picture_unknown);
+ }
+ }
+
+ /**
* Updates the info portion of the call card with passed in values for the primary user.
*/
private void updateInfoUiForPrimary(String displayName, String displayNumber, String label) {
diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
new file mode 100644
index 000000000..c9a331771
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2008 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.incallui;
+
+import android.app.Notification;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for loading contacts photo asynchronously.
+ */
+public class ContactsAsyncHelper {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "ContactsAsyncHelper";
+
+ /**
+ * Interface for a WorkerHandler result return.
+ */
+ public interface OnImageLoadCompleteListener {
+ /**
+ * Called when the image load is complete.
+ *
+ * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}.
+ * @param photo Drawable object obtained by the async load.
+ * @param photoIcon Bitmap object obtained by the async load.
+ * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
+ * cookie is null.
+ */
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
+ Object cookie);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+
+ private final Handler mResultHandler = new Handler() {
+ /** Called when loading is done. */
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ if (args.listener != null) {
+ if (DBG) {
+ Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
+ " image: " + args.uri + " completed");
+ }
+ args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
+ args.cookie);
+ }
+ break;
+ default:
+ }
+ }
+ };
+
+ /** Handler run on a worker thread to load photo asynchronously. */
+ private static Handler sThreadHandler;
+
+ /** For forcing the system to call its constructor */
+ @SuppressWarnings("unused")
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private static final class WorkerArgs {
+ public Context context;
+ public Uri uri;
+ public Drawable photo;
+ public Bitmap photoIcon;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ }
+
+ /**
+ * public inner class to help out the ContactsAsyncHelper callers
+ * with tracking the state of the CallerInfo Queries and image
+ * loading.
+ *
+ * Logic contained herein is used to remove the race conditions
+ * that exist as the CallerInfo queries run and mix with the image
+ * loads, which then mix with the Phone state changes.
+ */
+ public static class ImageTracker {
+
+ // Image display states
+ public static final int DISPLAY_UNDEFINED = 0;
+ public static final int DISPLAY_IMAGE = -1;
+ public static final int DISPLAY_DEFAULT = -2;
+
+ // State of the image on the imageview.
+ private CallerInfo mCurrentCallerInfo;
+ private int displayMode;
+
+ public ImageTracker() {
+ mCurrentCallerInfo = null;
+ displayMode = DISPLAY_UNDEFINED;
+ }
+
+ /**
+ * Used to see if the requested call / connection has a
+ * different caller attached to it than the one we currently
+ * have in the CallCard.
+ */
+ public boolean isDifferentImageRequest(CallerInfo ci) {
+ // note, since the connections are around for the lifetime of the
+ // call, and the CallerInfo-related items as well, we can
+ // definitely use a simple != comparison.
+ return (mCurrentCallerInfo != ci);
+ }
+
+ /**
+ * Simple setter for the CallerInfo object.
+ */
+ public void setPhotoRequest(CallerInfo info) {
+ mCurrentCallerInfo = info;
+ }
+
+ /**
+ * Convenience method used to retrieve the URI
+ * representing the Photo file recorded in the attached
+ * CallerInfo Object.
+ */
+ public Uri getPhotoUri() {
+ if (mCurrentCallerInfo != null) {
+ return ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ mCurrentCallerInfo.person_id);
+ }
+ return null;
+ }
+
+ /**
+ * Simple setter for the Photo state.
+ */
+ public void setPhotoState(int state) {
+ displayMode = state;
+ }
+
+ /**
+ * Simple getter for the Photo state.
+ */
+ public int getPhotoState() {
+ return displayMode;
+ }
+ }
+
+ /**
+ * Thread worker class that handles the task of opening the stream and loading
+ * the images.
+ */
+ private class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = Contacts.openContactPhotoInputStream(
+ args.context.getContentResolver(), args.uri, true);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Error opening photo input stream", e);
+ }
+
+ if (inputStream != null) {
+ args.photo = Drawable.createFromStream(inputStream,
+ args.uri.toString());
+
+ // This assumes Drawable coming from contact database is usually
+ // BitmapDrawable and thus we can have (down)scaled version of it.
+ args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+ if (DBG) {
+ Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri);
+ }
+ } else {
+ args.photo = null;
+ args.photoIcon = null;
+ if (DBG) {
+ Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri +
+ ", using default image.");
+ }
+ }
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unable to close input stream.", e);
+ }
+ }
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+
+ /**
+ * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
+ * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
+ * create a scaled Bitmap for the Drawable.
+ */
+ private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+ if (!(photo instanceof BitmapDrawable)) {
+ return null;
+ }
+ int iconSize = context.getResources()
+ .getDimensionPixelSize(R.dimen.notification_icon_size);
+ Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+ int orgWidth = orgBitmap.getWidth();
+ int orgHeight = orgBitmap.getHeight();
+ int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+ // We want downscaled one only when the original icon is too big.
+ if (longerEdge > iconSize) {
+ float ratio = ((float) longerEdge) / iconSize;
+ int newWidth = (int) (orgWidth / ratio);
+ int newHeight = (int) (orgHeight / ratio);
+ // If the longer edge is much longer than the shorter edge, the latter may
+ // become 0 which will cause a crash.
+ if (newWidth <= 0 || newHeight <= 0) {
+ Log.w(LOG_TAG, "Photo icon's width or height become 0.");
+ return null;
+ }
+
+ // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+ // should be smaller than the original.
+ return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+ } else {
+ return orgBitmap;
+ }
+ }
+ }
+
+ /**
+ * Private constructor for static class
+ */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Starts an asynchronous image load. After finishing the load,
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * will be called.
+ *
+ * @param token Arbitrary integer which will be returned as the first argument of
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * @param context Context object used to do the time-consuming operation.
+ * @param personUri Uri to be used to fetch the photo
+ * @param listener Callback object which will be used when the asynchronous load is done.
+ * Can be null, which means only the asynchronous load is done while there's no way to
+ * obtain the loaded photos.
+ * @param cookie Arbitrary object the caller wants to remember, which will become the
+ * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
+ * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
+ */
+ public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
+ OnImageLoadCompleteListener listener, Object cookie) {
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (personUri == null) {
+ Log.wtf(LOG_TAG, "Uri is missing");
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.uri = personUri;
+ args.listener = listener;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
+ ", displaying default image for now.");
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+
+}