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 android.pim; 18 19 import com.android.internal.telephony.CallerInfo; 20 import com.android.internal.telephony.Connection; 21 22 import android.content.ContentUris; 23 import android.content.Context; 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 import android.view.View; 33 import android.widget.ImageView; 34 35 import java.io.InputStream; 36 37 /** 38 * Helper class for async access of images. 39 */ 40 public class ContactsAsyncHelper extends Handler { 41 42 private static final boolean DBG = false; 43 private static final String LOG_TAG = "ContactsAsyncHelper"; 44 45 /** 46 * Interface for a WorkerHandler result return. 47 */ 48 public interface OnImageLoadCompleteListener { 49 /** 50 * Called when the image load is complete. 51 * 52 * @param imagePresent true if an image was found 53 */ 54 public void onImageLoadComplete(int token, Object cookie, ImageView iView, 55 boolean imagePresent); 56 } 57 58 // constants 59 private static final int EVENT_LOAD_IMAGE = 1; 60 private static final int DEFAULT_TOKEN = -1; 61 62 // static objects 63 private static Handler sThreadHandler; 64 private static ContactsAsyncHelper sInstance; 65 66 static { 67 sInstance = new ContactsAsyncHelper(); 68 } 69 70 private static final class WorkerArgs { 71 public Context context; 72 public ImageView view; 73 public Uri uri; 74 public int defaultResource; 75 public Object result; 76 public Object cookie; 77 public OnImageLoadCompleteListener listener; 78 public CallerInfo info; 79 } 80 81 /** 82 * public inner class to help out the ContactsAsyncHelper callers 83 * with tracking the state of the CallerInfo Queries and image 84 * loading. 85 * 86 * Logic contained herein is used to remove the race conditions 87 * that exist as the CallerInfo queries run and mix with the image 88 * loads, which then mix with the Phone state changes. 89 */ 90 public static class ImageTracker { 91 92 // Image display states 93 public static final int DISPLAY_UNDEFINED = 0; 94 public static final int DISPLAY_IMAGE = -1; 95 public static final int DISPLAY_DEFAULT = -2; 96 97 // State of the image on the imageview. 98 private CallerInfo mCurrentCallerInfo; 99 private int displayMode; 100 101 public ImageTracker() { 102 mCurrentCallerInfo = null; 103 displayMode = DISPLAY_UNDEFINED; 104 } 105 106 /** 107 * Used to see if the requested call / connection has a 108 * different caller attached to it than the one we currently 109 * have in the CallCard. 110 */ 111 public boolean isDifferentImageRequest(CallerInfo ci) { 112 // note, since the connections are around for the lifetime of the 113 // call, and the CallerInfo-related items as well, we can 114 // definitely use a simple != comparison. 115 return (mCurrentCallerInfo != ci); 116 } 117 118 public boolean isDifferentImageRequest(Connection connection) { 119 // if the connection does not exist, see if the 120 // mCurrentCallerInfo is also null to match. 121 if (connection == null) { 122 if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null"); 123 return (mCurrentCallerInfo != null); 124 } 125 Object o = connection.getUserData(); 126 127 // if the call does NOT have a callerInfo attached 128 // then it is ok to query. 129 boolean runQuery = true; 130 if (o instanceof CallerInfo) { 131 runQuery = isDifferentImageRequest((CallerInfo) o); 132 } 133 return runQuery; 134 } 135 136 /** 137 * Simple setter for the CallerInfo object. 138 */ 139 public void setPhotoRequest(CallerInfo ci) { 140 mCurrentCallerInfo = ci; 141 } 142 143 /** 144 * Convenience method used to retrieve the URI 145 * representing the Photo file recorded in the attached 146 * CallerInfo Object. 147 */ 148 public Uri getPhotoUri() { 149 if (mCurrentCallerInfo != null) { 150 return ContentUris.withAppendedId(Contacts.CONTENT_URI, 151 mCurrentCallerInfo.person_id); 152 } 153 return null; 154 } 155 156 /** 157 * Simple setter for the Photo state. 158 */ 159 public void setPhotoState(int state) { 160 displayMode = state; 161 } 162 163 /** 164 * Simple getter for the Photo state. 165 */ 166 public int getPhotoState() { 167 return displayMode; 168 } 169 } 170 171 /** 172 * Thread worker class that handles the task of opening the stream and loading 173 * the images. 174 */ 175 private class WorkerHandler extends Handler { 176 public WorkerHandler(Looper looper) { 177 super(looper); 178 } 179 180 @Override 181 public void handleMessage(Message msg) { 182 WorkerArgs args = (WorkerArgs) msg.obj; 183 184 switch (msg.arg1) { 185 case EVENT_LOAD_IMAGE: 186 InputStream inputStream = null; 187 try { 188 inputStream = Contacts.openContactPhotoInputStream( 189 args.context.getContentResolver(), args.uri); 190 } catch (Exception e) { 191 Log.e(LOG_TAG, "Error opening photo input stream", e); 192 } 193 194 if (inputStream != null) { 195 args.result = Drawable.createFromStream(inputStream, args.uri.toString()); 196 197 if (DBG) Log.d(LOG_TAG, "Loading image: " + msg.arg1 + 198 " token: " + msg.what + " image URI: " + args.uri); 199 } else { 200 args.result = null; 201 if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + 202 " token: " + msg.what + " image URI: " + args.uri + 203 ", using default image."); 204 } 205 break; 206 default: 207 } 208 209 // send the reply to the enclosing class. 210 Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what); 211 reply.arg1 = msg.arg1; 212 reply.obj = msg.obj; 213 reply.sendToTarget(); 214 } 215 } 216 217 /** 218 * Private constructor for static class 219 */ 220 private ContactsAsyncHelper() { 221 HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); 222 thread.start(); 223 sThreadHandler = new WorkerHandler(thread.getLooper()); 224 } 225 226 /** 227 * Convenience method for calls that do not want to deal with listeners and tokens. 228 */ 229 public static final void updateImageViewWithContactPhotoAsync(Context context, 230 ImageView imageView, Uri person, int placeholderImageResource) { 231 // Added additional Cookie field in the callee. 232 updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, 233 imageView, person, placeholderImageResource); 234 } 235 236 /** 237 * Convenience method for calls that do not want to deal with listeners and tokens, but have 238 * a CallerInfo object to cache the image to. 239 */ 240 public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, 241 ImageView imageView, Uri person, int placeholderImageResource) { 242 // Added additional Cookie field in the callee. 243 updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, 244 imageView, person, placeholderImageResource); 245 } 246 247 248 /** 249 * Start an image load, attach the result to the specified CallerInfo object. 250 * Note, when the query is started, we make the ImageView INVISIBLE if the 251 * placeholderImageResource value is -1. When we're given a valid (!= -1) 252 * placeholderImageResource value, we make sure the image is visible. 253 */ 254 public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, 255 OnImageLoadCompleteListener listener, Object cookie, Context context, 256 ImageView imageView, Uri person, int placeholderImageResource) { 257 258 // in case the source caller info is null, the URI will be null as well. 259 // just update using the placeholder image in this case. 260 if (person == null) { 261 if (DBG) Log.d(LOG_TAG, "target image is null, just display placeholder."); 262 imageView.setVisibility(View.VISIBLE); 263 imageView.setImageResource(placeholderImageResource); 264 return; 265 } 266 267 // Added additional Cookie field in the callee to handle arguments 268 // sent to the callback function. 269 270 // setup arguments 271 WorkerArgs args = new WorkerArgs(); 272 args.cookie = cookie; 273 args.context = context; 274 args.view = imageView; 275 args.uri = person; 276 args.defaultResource = placeholderImageResource; 277 args.listener = listener; 278 args.info = info; 279 280 // setup message arguments 281 Message msg = sThreadHandler.obtainMessage(token); 282 msg.arg1 = EVENT_LOAD_IMAGE; 283 msg.obj = args; 284 285 if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + 286 ", displaying default image for now."); 287 288 // set the default image first, when the query is complete, we will 289 // replace the image with the correct one. 290 if (placeholderImageResource != -1) { 291 imageView.setVisibility(View.VISIBLE); 292 imageView.setImageResource(placeholderImageResource); 293 } else { 294 imageView.setVisibility(View.INVISIBLE); 295 } 296 297 // notify the thread to begin working 298 sThreadHandler.sendMessage(msg); 299 } 300 301 /** 302 * Called when loading is done. 303 */ 304 @Override 305 public void handleMessage(Message msg) { 306 WorkerArgs args = (WorkerArgs) msg.obj; 307 switch (msg.arg1) { 308 case EVENT_LOAD_IMAGE: 309 boolean imagePresent = false; 310 311 // if the image has been loaded then display it, otherwise set default. 312 // in either case, make sure the image is visible. 313 if (args.result != null) { 314 args.view.setVisibility(View.VISIBLE); 315 args.view.setImageDrawable((Drawable) args.result); 316 // make sure the cached photo data is updated. 317 if (args.info != null) { 318 args.info.cachedPhoto = (Drawable) args.result; 319 } 320 imagePresent = true; 321 } else if (args.defaultResource != -1) { 322 args.view.setVisibility(View.VISIBLE); 323 args.view.setImageResource(args.defaultResource); 324 } 325 326 // Note that the data is cached. 327 if (args.info != null) { 328 args.info.isCachedPhotoCurrent = true; 329 } 330 331 // notify the listener if it is there. 332 if (args.listener != null) { 333 if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + 334 " image: " + args.uri + " completed"); 335 args.listener.onImageLoadComplete(msg.what, args.cookie, args.view, 336 imagePresent); 337 } 338 break; 339 default: 340 } 341 } 342 } 343