• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.phone;
18 
19 import android.app.Notification;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.drawable.Drawable;
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 import android.provider.ContactsContract.Contacts;
31 import android.util.Log;
32 
33 import com.android.internal.telephony.CallerInfo;
34 import com.android.internal.telephony.Connection;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 
39 /**
40  * Helper class for loading contacts photo asynchronously.
41  */
42 public 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          */
onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)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 
ImageTracker()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          */
isDifferentImageRequest(CallerInfo ci)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 
isDifferentImageRequest(Connection connection)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          */
setPhotoRequest(CallerInfo ci)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          */
getPhotoUri()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          */
setPhotoState(int state)188         public void setPhotoState(int state) {
189             displayMode = state;
190         }
191 
192         /**
193          * Simple getter for the Photo state.
194          */
getPhotoState()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 {
WorkerHandler(Looper looper)205         public WorkerHandler(Looper looper) {
206             super(looper);
207         }
208 
209         @Override
handleMessage(Message msg)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          */
getPhotoIconWhenAppropriate(Context context, Drawable photo)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      */
ContactsAsyncHelper()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      */
startObtainPhotoAsync(int token, Context context, Uri personUri, OnImageLoadCompleteListener listener, Object cookie)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 }
359