1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.incallui; 18 19 import android.app.Notification; 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.drawable.BitmapDrawable; 23 import android.graphics.drawable.Drawable; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.os.HandlerThread; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.support.annotation.MainThread; 30 import android.support.annotation.WorkerThread; 31 import java.io.IOException; 32 import java.io.InputStream; 33 34 /** Helper class for loading contacts photo asynchronously. */ 35 public class ContactsAsyncHelper { 36 37 /** Interface for a WorkerHandler result return. */ 38 public interface OnImageLoadCompleteListener { 39 40 /** 41 * Called when the image load is complete. Must be called in main thread. 42 * 43 * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, 44 * Uri, OnImageLoadCompleteListener, Object)}. 45 * @param photo Drawable object obtained by the async load. 46 * @param photoIcon Bitmap object obtained by the async load. 47 * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, 48 * Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null. 49 */ 50 @MainThread onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)51 void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie); 52 53 /** Called when image is loaded to udpate data. Must be called in worker thread. */ 54 @WorkerThread onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie)55 void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie); 56 } 57 58 // constants 59 private static final int EVENT_LOAD_IMAGE = 1; 60 /** Handler run on a worker thread to load photo asynchronously. */ 61 private static Handler sThreadHandler; 62 /** For forcing the system to call its constructor */ 63 @SuppressWarnings("unused") 64 private static ContactsAsyncHelper sInstance; 65 66 static { 67 sInstance = new ContactsAsyncHelper(); 68 } 69 70 private final Handler mResultHandler = 71 /** A handler that handles message to call listener notifying UI change on main thread. */ 72 new Handler(Looper.getMainLooper()) { 73 @Override 74 public void handleMessage(Message msg) { 75 WorkerArgs args = (WorkerArgs) msg.obj; 76 switch (msg.arg1) { 77 case EVENT_LOAD_IMAGE: 78 if (args.listener != null) { 79 Log.d( 80 this, 81 "Notifying listener: " 82 + args.listener.toString() 83 + " image: " 84 + args.displayPhotoUri 85 + " completed"); 86 args.listener.onImageLoadComplete( 87 msg.what, args.photo, args.photoIcon, args.cookie); 88 } 89 break; 90 default: 91 } 92 } 93 }; 94 95 /** Private constructor for static class */ ContactsAsyncHelper()96 private ContactsAsyncHelper() { 97 HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); 98 thread.start(); 99 sThreadHandler = new WorkerHandler(thread.getLooper()); 100 } 101 102 /** 103 * Starts an asynchronous image load. After finishing the load, {@link 104 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called. 105 * 106 * @param token Arbitrary integer which will be returned as the first argument of {@link 107 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 108 * @param context Context object used to do the time-consuming operation. 109 * @param displayPhotoUri Uri to be used to fetch the photo 110 * @param listener Callback object which will be used when the asynchronous load is done. Can be 111 * null, which means only the asynchronous load is done while there's no way to obtain the 112 * loaded photos. 113 * @param cookie Arbitrary object the caller wants to remember, which will become the fourth 114 * argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, 115 * Object)}. Can be null, at which the callback will also has null for the argument. 116 */ startObtainPhotoAsync( int token, Context context, Uri displayPhotoUri, OnImageLoadCompleteListener listener, Object cookie)117 public static final void startObtainPhotoAsync( 118 int token, 119 Context context, 120 Uri displayPhotoUri, 121 OnImageLoadCompleteListener listener, 122 Object cookie) { 123 // in case the source caller info is null, the URI will be null as well. 124 // just update using the placeholder image in this case. 125 if (displayPhotoUri == null) { 126 Log.e("startObjectPhotoAsync", "Uri is missing"); 127 return; 128 } 129 130 // Added additional Cookie field in the callee to handle arguments 131 // sent to the callback function. 132 133 // setup arguments 134 WorkerArgs args = new WorkerArgs(); 135 args.cookie = cookie; 136 args.context = context; 137 args.displayPhotoUri = displayPhotoUri; 138 args.listener = listener; 139 140 // setup message arguments 141 Message msg = sThreadHandler.obtainMessage(token); 142 msg.arg1 = EVENT_LOAD_IMAGE; 143 msg.obj = args; 144 145 Log.d( 146 "startObjectPhotoAsync", 147 "Begin loading image: " + args.displayPhotoUri + ", displaying default image for now."); 148 149 // notify the thread to begin working 150 sThreadHandler.sendMessage(msg); 151 } 152 153 private static final class WorkerArgs { 154 155 public Context context; 156 public Uri displayPhotoUri; 157 public Drawable photo; 158 public Bitmap photoIcon; 159 public Object cookie; 160 public OnImageLoadCompleteListener listener; 161 } 162 163 /** Thread worker class that handles the task of opening the stream and loading the images. */ 164 private class WorkerHandler extends Handler { 165 WorkerHandler(Looper looper)166 public WorkerHandler(Looper looper) { 167 super(looper); 168 } 169 170 @Override handleMessage(Message msg)171 public void handleMessage(Message msg) { 172 WorkerArgs args = (WorkerArgs) msg.obj; 173 174 switch (msg.arg1) { 175 case EVENT_LOAD_IMAGE: 176 InputStream inputStream = null; 177 try { 178 try { 179 inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri); 180 } catch (Exception e) { 181 Log.e(this, "Error opening photo input stream", e); 182 } 183 184 if (inputStream != null) { 185 args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString()); 186 187 // This assumes Drawable coming from contact database is usually 188 // BitmapDrawable and thus we can have (down)scaled version of it. 189 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); 190 191 Log.d( 192 ContactsAsyncHelper.this, 193 "Loading image: " 194 + msg.arg1 195 + " token: " 196 + msg.what 197 + " image URI: " 198 + args.displayPhotoUri); 199 } else { 200 args.photo = null; 201 args.photoIcon = null; 202 Log.d( 203 ContactsAsyncHelper.this, 204 "Problem with image: " 205 + msg.arg1 206 + " token: " 207 + msg.what 208 + " image URI: " 209 + args.displayPhotoUri 210 + ", using default image."); 211 } 212 if (args.listener != null) { 213 args.listener.onImageLoaded(msg.what, args.photo, args.photoIcon, args.cookie); 214 } 215 } finally { 216 if (inputStream != null) { 217 try { 218 inputStream.close(); 219 } catch (IOException e) { 220 Log.e(this, "Unable to close input stream.", e); 221 } 222 } 223 } 224 break; 225 default: 226 } 227 228 // send the reply to the enclosing class. 229 Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what); 230 reply.arg1 = msg.arg1; 231 reply.obj = msg.obj; 232 reply.sendToTarget(); 233 } 234 235 /** 236 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return 237 * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled 238 * Bitmap for the Drawable. 239 */ getPhotoIconWhenAppropriate(Context context, Drawable photo)240 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { 241 if (!(photo instanceof BitmapDrawable)) { 242 return null; 243 } 244 int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size); 245 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); 246 int orgWidth = orgBitmap.getWidth(); 247 int orgHeight = orgBitmap.getHeight(); 248 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; 249 // We want downscaled one only when the original icon is too big. 250 if (longerEdge > iconSize) { 251 float ratio = ((float) longerEdge) / iconSize; 252 int newWidth = (int) (orgWidth / ratio); 253 int newHeight = (int) (orgHeight / ratio); 254 // If the longer edge is much longer than the shorter edge, the latter may 255 // become 0 which will cause a crash. 256 if (newWidth <= 0 || newHeight <= 0) { 257 Log.w(this, "Photo icon's width or height become 0."); 258 return null; 259 } 260 261 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap 262 // should be smaller than the original. 263 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); 264 } else { 265 return orgBitmap; 266 } 267 } 268 } 269 } 270