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.phone; 18 19 import android.app.Notification; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.graphics.Bitmap; 23 import android.graphics.drawable.BitmapDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.HandlerThread; 28 import android.os.Looper; 29 import android.os.Message; 30 import android.provider.ContactsContract.Contacts; 31 import android.util.Log; 32 33 import com.android.internal.telephony.CallerInfo; 34 import com.android.internal.telephony.Connection; 35 36 import java.io.IOException; 37 import java.io.InputStream; 38 39 /** 40 * Helper class for loading contacts photo asynchronously. 41 */ 42 public class ContactsAsyncHelper { 43 44 private static final boolean DBG = false; 45 private static final String LOG_TAG = "ContactsAsyncHelper"; 46 47 /** 48 * Interface for a WorkerHandler result return. 49 */ 50 public interface OnImageLoadCompleteListener { 51 /** 52 * Called when the image load is complete. 53 * 54 * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, 55 * Context, Uri, OnImageLoadCompleteListener, Object)}. 56 * @param photo Drawable object obtained by the async load. 57 * @param photoIcon Bitmap object obtained by the async load. 58 * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, 59 * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original 60 * cookie is null. 61 */ 62 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, 63 Object cookie); 64 } 65 66 // constants 67 private static final int EVENT_LOAD_IMAGE = 1; 68 69 private final Handler mResultHandler = new Handler() { 70 /** Called when loading is done. */ 71 @Override 72 public void handleMessage(Message msg) { 73 WorkerArgs args = (WorkerArgs) msg.obj; 74 switch (msg.arg1) { 75 case EVENT_LOAD_IMAGE: 76 if (args.listener != null) { 77 if (DBG) { 78 Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + 79 " image: " + args.uri + " completed"); 80 } 81 args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon, 82 args.cookie); 83 } 84 break; 85 default: 86 } 87 } 88 }; 89 90 /** Handler run on a worker thread to load photo asynchronously. */ 91 private static Handler sThreadHandler; 92 93 /** For forcing the system to call its constructor */ 94 @SuppressWarnings("unused") 95 private static ContactsAsyncHelper sInstance; 96 97 static { 98 sInstance = new ContactsAsyncHelper(); 99 } 100 101 private static final class WorkerArgs { 102 public Context context; 103 public Uri uri; 104 public Drawable photo; 105 public Bitmap photoIcon; 106 public Object cookie; 107 public OnImageLoadCompleteListener listener; 108 } 109 110 /** 111 * public inner class to help out the ContactsAsyncHelper callers 112 * with tracking the state of the CallerInfo Queries and image 113 * loading. 114 * 115 * Logic contained herein is used to remove the race conditions 116 * that exist as the CallerInfo queries run and mix with the image 117 * loads, which then mix with the Phone state changes. 118 */ 119 public static class ImageTracker { 120 121 // Image display states 122 public static final int DISPLAY_UNDEFINED = 0; 123 public static final int DISPLAY_IMAGE = -1; 124 public static final int DISPLAY_DEFAULT = -2; 125 126 // State of the image on the imageview. 127 private CallerInfo mCurrentCallerInfo; 128 private int displayMode; 129 130 public ImageTracker() { 131 mCurrentCallerInfo = null; 132 displayMode = DISPLAY_UNDEFINED; 133 } 134 135 /** 136 * Used to see if the requested call / connection has a 137 * different caller attached to it than the one we currently 138 * have in the CallCard. 139 */ 140 public boolean isDifferentImageRequest(CallerInfo ci) { 141 // note, since the connections are around for the lifetime of the 142 // call, and the CallerInfo-related items as well, we can 143 // definitely use a simple != comparison. 144 return (mCurrentCallerInfo != ci); 145 } 146 147 public boolean isDifferentImageRequest(Connection connection) { 148 // if the connection does not exist, see if the 149 // mCurrentCallerInfo is also null to match. 150 if (connection == null) { 151 if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null"); 152 return (mCurrentCallerInfo != null); 153 } 154 Object o = connection.getUserData(); 155 156 // if the call does NOT have a callerInfo attached 157 // then it is ok to query. 158 boolean runQuery = true; 159 if (o instanceof CallerInfo) { 160 runQuery = isDifferentImageRequest((CallerInfo) o); 161 } 162 return runQuery; 163 } 164 165 /** 166 * Simple setter for the CallerInfo object. 167 */ 168 public void setPhotoRequest(CallerInfo ci) { 169 mCurrentCallerInfo = ci; 170 } 171 172 /** 173 * Convenience method used to retrieve the URI 174 * representing the Photo file recorded in the attached 175 * CallerInfo Object. 176 */ 177 public Uri getPhotoUri() { 178 if (mCurrentCallerInfo != null) { 179 return ContentUris.withAppendedId(Contacts.CONTENT_URI, 180 mCurrentCallerInfo.person_id); 181 } 182 return null; 183 } 184 185 /** 186 * Simple setter for the Photo state. 187 */ 188 public void setPhotoState(int state) { 189 displayMode = state; 190 } 191 192 /** 193 * Simple getter for the Photo state. 194 */ 195 public int getPhotoState() { 196 return displayMode; 197 } 198 } 199 200 /** 201 * Thread worker class that handles the task of opening the stream and loading 202 * the images. 203 */ 204 private class WorkerHandler extends Handler { 205 public WorkerHandler(Looper looper) { 206 super(looper); 207 } 208 209 @Override 210 public void handleMessage(Message msg) { 211 WorkerArgs args = (WorkerArgs) msg.obj; 212 213 switch (msg.arg1) { 214 case EVENT_LOAD_IMAGE: 215 InputStream inputStream = null; 216 try { 217 try { 218 inputStream = Contacts.openContactPhotoInputStream( 219 args.context.getContentResolver(), args.uri, true); 220 } catch (Exception e) { 221 Log.e(LOG_TAG, "Error opening photo input stream", e); 222 } 223 224 if (inputStream != null) { 225 args.photo = Drawable.createFromStream(inputStream, 226 args.uri.toString()); 227 228 // This assumes Drawable coming from contact database is usually 229 // BitmapDrawable and thus we can have (down)scaled version of it. 230 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); 231 232 if (DBG) { 233 Log.d(LOG_TAG, "Loading image: " + msg.arg1 + 234 " token: " + msg.what + " image URI: " + args.uri); 235 } 236 } else { 237 args.photo = null; 238 args.photoIcon = null; 239 if (DBG) { 240 Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + 241 " token: " + msg.what + " image URI: " + args.uri + 242 ", using default image."); 243 } 244 } 245 } finally { 246 if (inputStream != null) { 247 try { 248 inputStream.close(); 249 } catch (IOException e) { 250 Log.e(LOG_TAG, "Unable to close input stream.", e); 251 } 252 } 253 } 254 break; 255 default: 256 } 257 258 // send the reply to the enclosing class. 259 Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what); 260 reply.arg1 = msg.arg1; 261 reply.obj = msg.obj; 262 reply.sendToTarget(); 263 } 264 265 /** 266 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might 267 * return null when the given Drawable isn't BitmapDrawable, or if the system fails to 268 * create a scaled Bitmap for the Drawable. 269 */ 270 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { 271 if (!(photo instanceof BitmapDrawable)) { 272 return null; 273 } 274 int iconSize = context.getResources() 275 .getDimensionPixelSize(R.dimen.notification_icon_size); 276 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); 277 int orgWidth = orgBitmap.getWidth(); 278 int orgHeight = orgBitmap.getHeight(); 279 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; 280 // We want downscaled one only when the original icon is too big. 281 if (longerEdge > iconSize) { 282 float ratio = ((float) longerEdge) / iconSize; 283 int newWidth = (int) (orgWidth / ratio); 284 int newHeight = (int) (orgHeight / ratio); 285 // If the longer edge is much longer than the shorter edge, the latter may 286 // become 0 which will cause a crash. 287 if (newWidth <= 0 || newHeight <= 0) { 288 Log.w(LOG_TAG, "Photo icon's width or height become 0."); 289 return null; 290 } 291 292 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap 293 // should be smaller than the original. 294 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); 295 } else { 296 return orgBitmap; 297 } 298 } 299 } 300 301 /** 302 * Private constructor for static class 303 */ 304 private ContactsAsyncHelper() { 305 HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); 306 thread.start(); 307 sThreadHandler = new WorkerHandler(thread.getLooper()); 308 } 309 310 /** 311 * Starts an asynchronous image load. After finishing the load, 312 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 313 * will be called. 314 * 315 * @param token Arbitrary integer which will be returned as the first argument of 316 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 317 * @param context Context object used to do the time-consuming operation. 318 * @param personUri Uri to be used to fetch the photo 319 * @param listener Callback object which will be used when the asynchronous load is done. 320 * Can be null, which means only the asynchronous load is done while there's no way to 321 * obtain the loaded photos. 322 * @param cookie Arbitrary object the caller wants to remember, which will become the 323 * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, 324 * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument. 325 */ 326 public static final void startObtainPhotoAsync(int token, Context context, Uri personUri, 327 OnImageLoadCompleteListener listener, Object cookie) { 328 // in case the source caller info is null, the URI will be null as well. 329 // just update using the placeholder image in this case. 330 if (personUri == null) { 331 Log.wtf(LOG_TAG, "Uri is missing"); 332 return; 333 } 334 335 // Added additional Cookie field in the callee to handle arguments 336 // sent to the callback function. 337 338 // setup arguments 339 WorkerArgs args = new WorkerArgs(); 340 args.cookie = cookie; 341 args.context = context; 342 args.uri = personUri; 343 args.listener = listener; 344 345 // setup message arguments 346 Message msg = sThreadHandler.obtainMessage(token); 347 msg.arg1 = EVENT_LOAD_IMAGE; 348 msg.obj = args; 349 350 if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + 351 ", displaying default image for now."); 352 353 // notify the thread to begin working 354 sThreadHandler.sendMessage(msg); 355 } 356 357 358 } 359