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