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 */ onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent)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 ImageTracker()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 */ isDifferentImageRequest(CallerInfo ci)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 isDifferentImageRequest(Connection connection)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 */ setPhotoRequest(CallerInfo ci)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 */ getPhotoUri()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 */ setPhotoState(int state)159 public void setPhotoState(int state) { 160 displayMode = state; 161 } 162 163 /** 164 * Simple getter for the Photo state. 165 */ getPhotoState()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 { WorkerHandler(Looper looper)176 public WorkerHandler(Looper looper) { 177 super(looper); 178 } 179 180 @Override handleMessage(Message msg)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 */ ContactsAsyncHelper()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 */ updateImageViewWithContactPhotoAsync(Context context, ImageView imageView, Uri person, int placeholderImageResource)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 */ updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, ImageView imageView, Uri person, int placeholderImageResource)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 */ updateImageViewWithContactPhotoAsync(CallerInfo info, int token, OnImageLoadCompleteListener listener, Object cookie, Context context, ImageView imageView, Uri person, int placeholderImageResource)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 handleMessage(Message msg)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