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.content.res.AssetFileDescriptor; 23 import android.graphics.Bitmap; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.Looper; 30 import android.os.Message; 31 import android.provider.ContactsContract.Contacts; 32 33 import java.io.IOException; 34 import java.io.InputStream; 35 36 /** 37 * Helper class for loading contacts photo asynchronously. 38 */ 39 public class ContactsAsyncHelper { 40 41 /** 42 * Interface for a WorkerHandler result return. 43 */ 44 public interface OnImageLoadCompleteListener { 45 /** 46 * Called when the image load is complete. 47 * 48 * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, 49 * Context, Uri, OnImageLoadCompleteListener, Object)}. 50 * @param photo Drawable object obtained by the async load. 51 * @param photoIcon Bitmap object obtained by the async load. 52 * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, 53 * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original 54 * cookie is null. 55 */ 56 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, 57 Object cookie); 58 } 59 60 // constants 61 private static final int EVENT_LOAD_IMAGE = 1; 62 63 private final Handler mResultHandler = new Handler() { 64 /** Called when loading is done. */ 65 @Override 66 public void handleMessage(Message msg) { 67 WorkerArgs args = (WorkerArgs) msg.obj; 68 switch (msg.arg1) { 69 case EVENT_LOAD_IMAGE: 70 if (args.listener != null) { 71 Log.d(this, "Notifying listener: " + args.listener.toString() + 72 " image: " + args.displayPhotoUri + " completed"); 73 args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon, 74 args.cookie); 75 } 76 break; 77 default: 78 } 79 } 80 }; 81 82 /** Handler run on a worker thread to load photo asynchronously. */ 83 private static Handler sThreadHandler; 84 85 /** For forcing the system to call its constructor */ 86 @SuppressWarnings("unused") 87 private static ContactsAsyncHelper sInstance; 88 89 static { 90 sInstance = new ContactsAsyncHelper(); 91 } 92 93 private static final class WorkerArgs { 94 public Context context; 95 public Uri displayPhotoUri; 96 public Drawable photo; 97 public Bitmap photoIcon; 98 public Object cookie; 99 public OnImageLoadCompleteListener listener; 100 } 101 102 /** 103 * Thread worker class that handles the task of opening the stream and loading 104 * the images. 105 */ 106 private class WorkerHandler extends Handler { 107 public WorkerHandler(Looper looper) { 108 super(looper); 109 } 110 111 @Override 112 public void handleMessage(Message msg) { 113 WorkerArgs args = (WorkerArgs) msg.obj; 114 115 switch (msg.arg1) { 116 case EVENT_LOAD_IMAGE: 117 InputStream inputStream = null; 118 try { 119 try { 120 inputStream = args.context.getContentResolver() 121 .openInputStream(args.displayPhotoUri); 122 } catch (Exception e) { 123 Log.e(this, "Error opening photo input stream", e); 124 } 125 126 if (inputStream != null) { 127 args.photo = Drawable.createFromStream(inputStream, 128 args.displayPhotoUri.toString()); 129 130 // This assumes Drawable coming from contact database is usually 131 // BitmapDrawable and thus we can have (down)scaled version of it. 132 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); 133 134 Log.d(ContactsAsyncHelper.this, "Loading image: " + msg.arg1 + 135 " token: " + msg.what + " image URI: " + args.displayPhotoUri); 136 } else { 137 args.photo = null; 138 args.photoIcon = null; 139 Log.d(ContactsAsyncHelper.this, "Problem with image: " + msg.arg1 + 140 " token: " + msg.what + " image URI: " + args.displayPhotoUri + 141 ", using default image."); 142 } 143 } finally { 144 if (inputStream != null) { 145 try { 146 inputStream.close(); 147 } catch (IOException e) { 148 Log.e(this, "Unable to close input stream.", e); 149 } 150 } 151 } 152 break; 153 default: 154 } 155 156 // send the reply to the enclosing class. 157 Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what); 158 reply.arg1 = msg.arg1; 159 reply.obj = msg.obj; 160 reply.sendToTarget(); 161 } 162 163 /** 164 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might 165 * return null when the given Drawable isn't BitmapDrawable, or if the system fails to 166 * create a scaled Bitmap for the Drawable. 167 */ 168 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { 169 if (!(photo instanceof BitmapDrawable)) { 170 return null; 171 } 172 int iconSize = context.getResources() 173 .getDimensionPixelSize(R.dimen.notification_icon_size); 174 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); 175 int orgWidth = orgBitmap.getWidth(); 176 int orgHeight = orgBitmap.getHeight(); 177 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; 178 // We want downscaled one only when the original icon is too big. 179 if (longerEdge > iconSize) { 180 float ratio = ((float) longerEdge) / iconSize; 181 int newWidth = (int) (orgWidth / ratio); 182 int newHeight = (int) (orgHeight / ratio); 183 // If the longer edge is much longer than the shorter edge, the latter may 184 // become 0 which will cause a crash. 185 if (newWidth <= 0 || newHeight <= 0) { 186 Log.w(this, "Photo icon's width or height become 0."); 187 return null; 188 } 189 190 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap 191 // should be smaller than the original. 192 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); 193 } else { 194 return orgBitmap; 195 } 196 } 197 } 198 199 /** 200 * Private constructor for static class 201 */ 202 private ContactsAsyncHelper() { 203 HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); 204 thread.start(); 205 sThreadHandler = new WorkerHandler(thread.getLooper()); 206 } 207 208 /** 209 * Starts an asynchronous image load. After finishing the load, 210 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 211 * will be called. 212 * 213 * @param token Arbitrary integer which will be returned as the first argument of 214 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 215 * @param context Context object used to do the time-consuming operation. 216 * @param displayPhotoUri Uri to be used to fetch the photo 217 * @param listener Callback object which will be used when the asynchronous load is done. 218 * Can be null, which means only the asynchronous load is done while there's no way to 219 * obtain the loaded photos. 220 * @param cookie Arbitrary object the caller wants to remember, which will become the 221 * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, 222 * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument. 223 */ 224 public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri, 225 OnImageLoadCompleteListener listener, Object cookie) { 226 // in case the source caller info is null, the URI will be null as well. 227 // just update using the placeholder image in this case. 228 if (displayPhotoUri == null) { 229 Log.wtf("startObjectPhotoAsync", "Uri is missing"); 230 return; 231 } 232 233 // Added additional Cookie field in the callee to handle arguments 234 // sent to the callback function. 235 236 // setup arguments 237 WorkerArgs args = new WorkerArgs(); 238 args.cookie = cookie; 239 args.context = context; 240 args.displayPhotoUri = displayPhotoUri; 241 args.listener = listener; 242 243 // setup message arguments 244 Message msg = sThreadHandler.obtainMessage(token); 245 msg.arg1 = EVENT_LOAD_IMAGE; 246 msg.obj = args; 247 248 Log.d("startObjectPhotoAsync", "Begin loading image: " + args.displayPhotoUri + 249 ", displaying default image for now."); 250 251 // notify the thread to begin working 252 sThreadHandler.sendMessage(msg); 253 } 254 255 256 } 257