blob: 10a69503ec94de3ba4fe1bda6be43f4aeceffa32 [file] [log] [blame]
Santos Cordon7d4ddf62013-07-10 11:58:08 -07001/*
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.phone;
18
19import android.app.Notification;
20import android.content.ContentUris;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.graphics.drawable.BitmapDrawable;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.os.Looper;
29import android.os.Message;
30import android.provider.ContactsContract.Contacts;
31import android.util.Log;
32
33import com.android.internal.telephony.CallerInfo;
34import com.android.internal.telephony.Connection;
35
36import java.io.IOException;
37import java.io.InputStream;
38
39/**
40 * Helper class for loading contacts photo asynchronously.
41 */
42public class ContactsAsyncHelper {
43
44 private static final boolean DBG = false;
45 private static final String LOG_TAG = "ContactsAsyncHelper";
46
47 /**
48 * Interface for a WorkerHandler result return.
49 */
50 public interface OnImageLoadCompleteListener {
51 /**
52 * Called when the image load is complete.
53 *
54 * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
55 * Context, Uri, OnImageLoadCompleteListener, Object)}.
56 * @param photo Drawable object obtained by the async load.
57 * @param photoIcon Bitmap object obtained by the async load.
58 * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
59 * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
60 * cookie is null.
61 */
62 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
63 Object cookie);
64 }
65
66 // constants
67 private static final int EVENT_LOAD_IMAGE = 1;
68
69 private final Handler mResultHandler = new Handler() {
70 /** Called when loading is done. */
71 @Override
72 public void handleMessage(Message msg) {
73 WorkerArgs args = (WorkerArgs) msg.obj;
74 switch (msg.arg1) {
75 case EVENT_LOAD_IMAGE:
76 if (args.listener != null) {
77 if (DBG) {
78 Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
79 " image: " + args.uri + " completed");
80 }
81 args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
82 args.cookie);
83 }
84 break;
85 default:
86 }
87 }
88 };
89
90 /** Handler run on a worker thread to load photo asynchronously. */
91 private static Handler sThreadHandler;
92
93 /** For forcing the system to call its constructor */
94 @SuppressWarnings("unused")
95 private static ContactsAsyncHelper sInstance;
96
97 static {
98 sInstance = new ContactsAsyncHelper();
99 }
100
101 private static final class WorkerArgs {
102 public Context context;
103 public Uri uri;
104 public Drawable photo;
105 public Bitmap photoIcon;
106 public Object cookie;
107 public OnImageLoadCompleteListener listener;
108 }
109
110 /**
111 * public inner class to help out the ContactsAsyncHelper callers
112 * with tracking the state of the CallerInfo Queries and image
113 * loading.
114 *
115 * Logic contained herein is used to remove the race conditions
116 * that exist as the CallerInfo queries run and mix with the image
117 * loads, which then mix with the Phone state changes.
118 */
119 public static class ImageTracker {
120
121 // Image display states
122 public static final int DISPLAY_UNDEFINED = 0;
123 public static final int DISPLAY_IMAGE = -1;
124 public static final int DISPLAY_DEFAULT = -2;
125
126 // State of the image on the imageview.
127 private CallerInfo mCurrentCallerInfo;
128 private int displayMode;
129
130 public ImageTracker() {
131 mCurrentCallerInfo = null;
132 displayMode = DISPLAY_UNDEFINED;
133 }
134
135 /**
136 * Used to see if the requested call / connection has a
137 * different caller attached to it than the one we currently
138 * have in the CallCard.
139 */
140 public boolean isDifferentImageRequest(CallerInfo ci) {
141 // note, since the connections are around for the lifetime of the
142 // call, and the CallerInfo-related items as well, we can
143 // definitely use a simple != comparison.
144 return (mCurrentCallerInfo != ci);
145 }
146
147 public boolean isDifferentImageRequest(Connection connection) {
148 // if the connection does not exist, see if the
149 // mCurrentCallerInfo is also null to match.
150 if (connection == null) {
151 if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
152 return (mCurrentCallerInfo != null);
153 }
154 Object o = connection.getUserData();
155
156 // if the call does NOT have a callerInfo attached
157 // then it is ok to query.
158 boolean runQuery = true;
159 if (o instanceof CallerInfo) {
160 runQuery = isDifferentImageRequest((CallerInfo) o);
161 }
162 return runQuery;
163 }
164
165 /**
166 * Simple setter for the CallerInfo object.
167 */
168 public void setPhotoRequest(CallerInfo ci) {
169 mCurrentCallerInfo = ci;
170 }
171
172 /**
173 * Convenience method used to retrieve the URI
174 * representing the Photo file recorded in the attached
175 * CallerInfo Object.
176 */
177 public Uri getPhotoUri() {
178 if (mCurrentCallerInfo != null) {
179 return ContentUris.withAppendedId(Contacts.CONTENT_URI,
180 mCurrentCallerInfo.person_id);
181 }
182 return null;
183 }
184
185 /**
186 * Simple setter for the Photo state.
187 */
188 public void setPhotoState(int state) {
189 displayMode = state;
190 }
191
192 /**
193 * Simple getter for the Photo state.
194 */
195 public int getPhotoState() {
196 return displayMode;
197 }
198 }
199
200 /**
201 * Thread worker class that handles the task of opening the stream and loading
202 * the images.
203 */
204 private class WorkerHandler extends Handler {
205 public WorkerHandler(Looper looper) {
206 super(looper);
207 }
208
209 @Override
210 public void handleMessage(Message msg) {
211 WorkerArgs args = (WorkerArgs) msg.obj;
212
213 switch (msg.arg1) {
214 case EVENT_LOAD_IMAGE:
215 InputStream inputStream = null;
216 try {
217 try {
218 inputStream = Contacts.openContactPhotoInputStream(
219 args.context.getContentResolver(), args.uri, true);
220 } catch (Exception e) {
221 Log.e(LOG_TAG, "Error opening photo input stream", e);
222 }
223
224 if (inputStream != null) {
225 args.photo = Drawable.createFromStream(inputStream,
226 args.uri.toString());
227
228 // This assumes Drawable coming from contact database is usually
229 // BitmapDrawable and thus we can have (down)scaled version of it.
230 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
231
232 if (DBG) {
233 Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
234 " token: " + msg.what + " image URI: " + args.uri);
235 }
236 } else {
237 args.photo = null;
238 args.photoIcon = null;
239 if (DBG) {
240 Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
241 " token: " + msg.what + " image URI: " + args.uri +
242 ", using default image.");
243 }
244 }
245 } finally {
246 if (inputStream != null) {
247 try {
248 inputStream.close();
249 } catch (IOException e) {
250 Log.e(LOG_TAG, "Unable to close input stream.", e);
251 }
252 }
253 }
254 break;
255 default:
256 }
257
258 // send the reply to the enclosing class.
259 Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
260 reply.arg1 = msg.arg1;
261 reply.obj = msg.obj;
262 reply.sendToTarget();
263 }
264
265 /**
266 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
267 * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
268 * create a scaled Bitmap for the Drawable.
269 */
270 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
271 if (!(photo instanceof BitmapDrawable)) {
272 return null;
273 }
274 int iconSize = context.getResources()
275 .getDimensionPixelSize(R.dimen.notification_icon_size);
276 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
277 int orgWidth = orgBitmap.getWidth();
278 int orgHeight = orgBitmap.getHeight();
279 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
280 // We want downscaled one only when the original icon is too big.
281 if (longerEdge > iconSize) {
282 float ratio = ((float) longerEdge) / iconSize;
283 int newWidth = (int) (orgWidth / ratio);
284 int newHeight = (int) (orgHeight / ratio);
285 // If the longer edge is much longer than the shorter edge, the latter may
286 // become 0 which will cause a crash.
287 if (newWidth <= 0 || newHeight <= 0) {
288 Log.w(LOG_TAG, "Photo icon's width or height become 0.");
289 return null;
290 }
291
292 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
293 // should be smaller than the original.
294 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
295 } else {
296 return orgBitmap;
297 }
298 }
299 }
300
301 /**
302 * Private constructor for static class
303 */
304 private ContactsAsyncHelper() {
305 HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
306 thread.start();
307 sThreadHandler = new WorkerHandler(thread.getLooper());
308 }
309
310 /**
311 * Starts an asynchronous image load. After finishing the load,
312 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
313 * will be called.
314 *
315 * @param token Arbitrary integer which will be returned as the first argument of
316 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
317 * @param context Context object used to do the time-consuming operation.
318 * @param personUri Uri to be used to fetch the photo
319 * @param listener Callback object which will be used when the asynchronous load is done.
320 * Can be null, which means only the asynchronous load is done while there's no way to
321 * obtain the loaded photos.
322 * @param cookie Arbitrary object the caller wants to remember, which will become the
323 * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
324 * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
325 */
326 public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
327 OnImageLoadCompleteListener listener, Object cookie) {
328 // in case the source caller info is null, the URI will be null as well.
329 // just update using the placeholder image in this case.
330 if (personUri == null) {
331 Log.wtf(LOG_TAG, "Uri is missing");
332 return;
333 }
334
335 // Added additional Cookie field in the callee to handle arguments
336 // sent to the callback function.
337
338 // setup arguments
339 WorkerArgs args = new WorkerArgs();
340 args.cookie = cookie;
341 args.context = context;
342 args.uri = personUri;
343 args.listener = listener;
344
345 // setup message arguments
346 Message msg = sThreadHandler.obtainMessage(token);
347 msg.arg1 = EVENT_LOAD_IMAGE;
348 msg.obj = args;
349
350 if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
351 ", displaying default image for now.");
352
353 // notify the thread to begin working
354 sThreadHandler.sendMessage(msg);
355 }
356
357
358}