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