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