diff options
Diffstat (limited to 'java/com/android/incallui/ContactsAsyncHelper.java')
-rw-r--r-- | java/com/android/incallui/ContactsAsyncHelper.java | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/java/com/android/incallui/ContactsAsyncHelper.java b/java/com/android/incallui/ContactsAsyncHelper.java new file mode 100644 index 000000000..08ff74d0e --- /dev/null +++ b/java/com/android/incallui/ContactsAsyncHelper.java @@ -0,0 +1,269 @@ +/* + * 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.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.support.annotation.MainThread; +import android.support.annotation.WorkerThread; +import java.io.IOException; +import java.io.InputStream; + +/** Helper class for loading contacts photo asynchronously. */ +public class ContactsAsyncHelper { + + /** Interface for a WorkerHandler result return. */ + public interface OnImageLoadCompleteListener { + + /** + * Called when the image load is complete. Must be called in main thread. + * + * @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. + */ + @MainThread + void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie); + + /** Called when image is loaded to udpate data. Must be called in worker thread. */ + @WorkerThread + void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie); + } + + // constants + private static final int EVENT_LOAD_IMAGE = 1; + /** 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 final Handler mResultHandler = + /** A handler that handles message to call listener notifying UI change on main thread. */ + new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + WorkerArgs args = (WorkerArgs) msg.obj; + switch (msg.arg1) { + case EVENT_LOAD_IMAGE: + if (args.listener != null) { + Log.d( + this, + "Notifying listener: " + + args.listener.toString() + + " image: " + + args.displayPhotoUri + + " completed"); + args.listener.onImageLoadComplete( + msg.what, args.photo, args.photoIcon, args.cookie); + } + break; + default: + } + } + }; + + /** 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 displayPhotoUri 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 displayPhotoUri, + 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 (displayPhotoUri == null) { + Log.e("startObjectPhotoAsync", "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.displayPhotoUri = displayPhotoUri; + args.listener = listener; + + // setup message arguments + Message msg = sThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_LOAD_IMAGE; + msg.obj = args; + + Log.d( + "startObjectPhotoAsync", + "Begin loading image: " + args.displayPhotoUri + ", displaying default image for now."); + + // notify the thread to begin working + sThreadHandler.sendMessage(msg); + } + + private static final class WorkerArgs { + + public Context context; + public Uri displayPhotoUri; + public Drawable photo; + public Bitmap photoIcon; + public Object cookie; + public OnImageLoadCompleteListener listener; + } + + /** 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 = args.context.getContentResolver().openInputStream(args.displayPhotoUri); + } catch (Exception e) { + Log.e(this, "Error opening photo input stream", e); + } + + if (inputStream != null) { + args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.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); + + Log.d( + ContactsAsyncHelper.this, + "Loading image: " + + msg.arg1 + + " token: " + + msg.what + + " image URI: " + + args.displayPhotoUri); + } else { + args.photo = null; + args.photoIcon = null; + Log.d( + ContactsAsyncHelper.this, + "Problem with image: " + + msg.arg1 + + " token: " + + msg.what + + " image URI: " + + args.displayPhotoUri + + ", using default image."); + } + if (args.listener != null) { + args.listener.onImageLoaded(msg.what, args.photo, args.photoIcon, args.cookie); + } + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(this, "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(this, "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; + } + } + } +} |