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