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