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