blob: da0aa038ad08e3f8dd89004cd390171738f1eb96 [file] [log] [blame]
Santos Cordon99c8a6f2014-05-28 18:28:47 -07001/*
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
17package com.android.telecomm;
18
19import android.app.Notification;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.drawable.BitmapDrawable;
23import android.graphics.drawable.Drawable;
24import android.net.Uri;
25import android.os.Handler;
26import android.os.HandlerThread;
27import android.os.Looper;
28import android.os.Message;
29import android.provider.ContactsContract.Contacts;
30
31import java.io.IOException;
32import java.io.InputStream;
33
34/**
35 * Helper class for loading contacts photo asynchronously.
36 */
37public final class ContactsAsyncHelper {
38 private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();
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 */
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 static final Handler sResultHandler = new Handler(Looper.getMainLooper()) {
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() +
Makoto Onukia1662d02014-07-10 15:31:59 -070071 " image: " + args.displayPhotoUri + " completed");
Santos Cordon99c8a6f2014-05-28 18:28:47 -070072 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 final Handler sThreadHandler;
83
84 static {
85 HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
86 thread.start();
87 sThreadHandler = new WorkerHandler(thread.getLooper());
88 }
89
90 private ContactsAsyncHelper() {}
91
92 private static final class WorkerArgs {
93 public Context context;
Makoto Onukia1662d02014-07-10 15:31:59 -070094 public Uri displayPhotoUri;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070095 public Drawable photo;
96 public Bitmap photoIcon;
97 public Object cookie;
98 public OnImageLoadCompleteListener listener;
99 }
100
101 /**
102 * Thread worker class that handles the task of opening the stream and loading
103 * the images.
104 */
105 private static class WorkerHandler extends Handler {
106 public WorkerHandler(Looper looper) {
107 super(looper);
108 }
109
110 @Override
111 public void handleMessage(Message msg) {
112 WorkerArgs args = (WorkerArgs) msg.obj;
113
114 switch (msg.arg1) {
115 case EVENT_LOAD_IMAGE:
116 InputStream inputStream = null;
117 try {
118 try {
Makoto Onukia1662d02014-07-10 15:31:59 -0700119 inputStream = args.context.getContentResolver()
120 .openInputStream(args.displayPhotoUri);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700121 } catch (Exception e) {
122 Log.e(this, e, "Error opening photo input stream");
123 }
124
125 if (inputStream != null) {
126 args.photo = Drawable.createFromStream(inputStream,
Makoto Onukia1662d02014-07-10 15:31:59 -0700127 args.displayPhotoUri.toString());
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700128
129 // This assumes Drawable coming from contact database is usually
130 // BitmapDrawable and thus we can have (down)scaled version of it.
131 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
132
133 Log.d(this, "Loading image: " + msg.arg1 +
Makoto Onukia1662d02014-07-10 15:31:59 -0700134 " token: " + msg.what + " image URI: " + args.displayPhotoUri);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700135 } else {
136 args.photo = null;
137 args.photoIcon = null;
138 Log.d(this, "Problem with image: " + msg.arg1 +
Makoto Onukia1662d02014-07-10 15:31:59 -0700139 " token: " + msg.what + " image URI: " + args.displayPhotoUri +
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700140 ", using default image.");
141 }
142 } finally {
143 if (inputStream != null) {
144 try {
145 inputStream.close();
146 } catch (IOException e) {
147 Log.e(this, e, "Unable to close input stream.");
148 }
149 }
150 }
151 break;
152 default:
153 }
154
155 // send the reply to the enclosing class.
156 Message reply = sResultHandler.obtainMessage(msg.what);
157 reply.arg1 = msg.arg1;
158 reply.obj = msg.obj;
159 reply.sendToTarget();
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 */
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.
Makoto Onukia1662d02014-07-10 15:31:59 -0700206 * @param displayPhotoUri Uri to be used to fetch the photo
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700207 * @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 */
Makoto Onukia1662d02014-07-10 15:31:59 -0700214 public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700215 OnImageLoadCompleteListener listener, Object cookie) {
216 ThreadUtil.checkOnMainThread();
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.
Makoto Onukia1662d02014-07-10 15:31:59 -0700220 if (displayPhotoUri == null) {
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700221 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;
Makoto Onukia1662d02014-07-10 15:31:59 -0700232 args.displayPhotoUri = displayPhotoUri;
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700233 args.listener = listener;
234
235 // setup message arguments
236 Message msg = sThreadHandler.obtainMessage(token);
237 msg.arg1 = EVENT_LOAD_IMAGE;
238 msg.obj = args;
239
Makoto Onukia1662d02014-07-10 15:31:59 -0700240 Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700241 ", displaying default image for now.");
242
243 // notify the thread to begin working
244 sThreadHandler.sendMessage(msg);
245 }
246
247
248}