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