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