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