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