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