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.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.drawable.BitmapDrawable; 23 import android.graphics.drawable.Drawable; 24 import android.net.Uri; 25 import android.support.annotation.MainThread; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.WorkerThread; 28 import com.android.dialer.common.LogUtil; 29 import com.android.dialer.common.concurrent.DialerExecutor; 30 import com.android.dialer.common.concurrent.DialerExecutorComponent; 31 import java.io.IOException; 32 import java.io.InputStream; 33 34 /** Helper class for loading contacts photo asynchronously. */ 35 public class ContactsAsyncHelper { 36 37 /** Interface for a WorkerHandler result return. */ 38 interface OnImageLoadCompleteListener { 39 40 /** 41 * Called when the image load is complete. Must be called in main thread. 42 * 43 * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, 44 * Uri, OnImageLoadCompleteListener, Object)}. 45 * @param photo Drawable object obtained by the async load. 46 * @param photoIcon Bitmap object obtained by the async load. 47 * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, 48 * Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null. 49 */ 50 @MainThread onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)51 void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie); 52 53 /** Called when image is loaded to udpate data. Must be called in worker thread. */ 54 @WorkerThread onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie)55 void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie); 56 } 57 58 /** 59 * Starts an asynchronous image load. After finishing the load, {@link 60 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called. 61 * 62 * @param token Arbitrary integer which will be returned as the first argument of {@link 63 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} 64 * @param context Context object used to do the time-consuming operation. 65 * @param displayPhotoUri Uri to be used to fetch the photo 66 * @param listener Callback object which will be used when the asynchronous load is done. Can be 67 * null, which means only the asynchronous load is done while there's no way to obtain the 68 * loaded photos. 69 * @param cookie Arbitrary object the caller wants to remember, which will become the fourth 70 * argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, 71 * Object)}. Can be null, at which the callback will also has null for the argument. 72 */ startObtainPhotoAsync( int token, Context context, Uri displayPhotoUri, OnImageLoadCompleteListener listener, Object cookie)73 static void startObtainPhotoAsync( 74 int token, 75 Context context, 76 Uri displayPhotoUri, 77 OnImageLoadCompleteListener listener, 78 Object cookie) { 79 // in case the source caller info is null, the URI will be null as well. 80 // just update using the placeholder image in this case. 81 if (displayPhotoUri == null) { 82 LogUtil.e("ContactsAsyncHelper.startObjectPhotoAsync", "uri is missing"); 83 return; 84 } 85 86 // Added additional Cookie field in the callee to handle arguments 87 // sent to the callback function. 88 89 // setup arguments 90 WorkerArgs args = new WorkerArgs(); 91 args.token = token; 92 args.cookie = cookie; 93 args.context = context; 94 args.displayPhotoUri = displayPhotoUri; 95 args.listener = listener; 96 97 DialerExecutorComponent.get(context) 98 .dialerExecutorFactory() 99 .createNonUiTaskBuilder(new Worker()) 100 .onSuccess( 101 output -> { 102 if (args.listener != null) { 103 LogUtil.d( 104 "ContactsAsyncHelper.startObtainPhotoAsync", 105 "notifying listener: " 106 + args.listener 107 + " image: " 108 + args.displayPhotoUri 109 + " completed"); 110 args.listener.onImageLoadComplete( 111 args.token, args.photo, args.photoIcon, args.cookie); 112 } 113 }) 114 .build() 115 .executeParallel(args); 116 } 117 118 private static final class WorkerArgs { 119 120 public int token; 121 public Context context; 122 public Uri displayPhotoUri; 123 public Drawable photo; 124 public Bitmap photoIcon; 125 public Object cookie; 126 public OnImageLoadCompleteListener listener; 127 } 128 129 private static class Worker implements DialerExecutor.Worker<WorkerArgs, Void> { 130 131 @Nullable 132 @Override doInBackground(WorkerArgs args)133 public Void doInBackground(WorkerArgs args) throws Throwable { 134 InputStream inputStream = null; 135 try { 136 try { 137 inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri); 138 } catch (Exception e) { 139 LogUtil.e( 140 "ContactsAsyncHelper.Worker.doInBackground", "error opening photo input stream", e); 141 } 142 143 if (inputStream != null) { 144 args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString()); 145 146 // This assumes Drawable coming from contact database is usually 147 // BitmapDrawable and thus we can have (down)scaled version of it. 148 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); 149 150 LogUtil.d( 151 "ContactsAsyncHelper.Worker.doInBackground", 152 "loading image, URI: %s", 153 args.displayPhotoUri); 154 } else { 155 args.photo = null; 156 args.photoIcon = null; 157 LogUtil.d( 158 "ContactsAsyncHelper.Worker.doInBackground", 159 "problem with image, URI: %s, using default image.", 160 args.displayPhotoUri); 161 } 162 if (args.listener != null) { 163 args.listener.onImageLoaded(args.token, args.photo, args.photoIcon, args.cookie); 164 } 165 } finally { 166 if (inputStream != null) { 167 try { 168 inputStream.close(); 169 } catch (IOException e) { 170 LogUtil.e( 171 "ContactsAsyncHelper.Worker.doInBackground", "Unable to close input stream.", e); 172 } 173 } 174 } 175 return null; 176 } 177 178 /** 179 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return 180 * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled 181 * Bitmap for the Drawable. 182 */ getPhotoIconWhenAppropriate(Context context, Drawable photo)183 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { 184 if (!(photo instanceof BitmapDrawable)) { 185 return null; 186 } 187 int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size); 188 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); 189 int orgWidth = orgBitmap.getWidth(); 190 int orgHeight = orgBitmap.getHeight(); 191 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; 192 // We want downscaled one only when the original icon is too big. 193 if (longerEdge > iconSize) { 194 float ratio = ((float) longerEdge) / iconSize; 195 int newWidth = (int) (orgWidth / ratio); 196 int newHeight = (int) (orgHeight / ratio); 197 // If the longer edge is much longer than the shorter edge, the latter may 198 // become 0 which will cause a crash. 199 if (newWidth <= 0 || newHeight <= 0) { 200 LogUtil.w( 201 "ContactsAsyncHelper.Worker.getPhotoIconWhenAppropriate", 202 "Photo icon's width or height become 0."); 203 return null; 204 } 205 206 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap 207 // should be smaller than the original. 208 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); 209 } else { 210 return orgBitmap; 211 } 212 } 213 } 214 } 215