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