diff options
-rw-r--r-- | InCallUI/res/values/ids.xml | 19 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/AnimationUtils.java | 279 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallCardFragment.java | 31 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallCardPresenter.java | 110 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/ContactsAsyncHelper.java | 337 |
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); + } + + +} |