blob: 7fb6419f1384d7e6a29a89e9eb4b777f44b90ade [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
Tyler Gunn7cc70b42014-09-12 22:17:27 -070017package com.android.server.telecom;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070018
19import android.app.Notification;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.drawable.BitmapDrawable;
23import android.graphics.drawable.Drawable;
Brad Ebingera3eccfe2016-10-05 15:45:22 -070024import android.telecom.Log;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070025import android.net.Uri;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.os.Looper;
29import android.os.Message;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070030
Tyler Gunn91d43cf2014-09-17 12:19:39 -070031// TODO: Needed for move to system service: import com.android.internal.R;
32
Hall Liu9ccc43d2015-12-15 14:18:33 -080033import java.io.FileNotFoundException;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070034import java.io.IOException;
35import java.io.InputStream;
36
37/**
38 * Helper class for loading contacts photo asynchronously.
39 */
Hall Liu5b70c1c2016-03-03 18:42:57 -080040public class ContactsAsyncHelper {
Santos Cordon99c8a6f2014-05-28 18:28:47 -070041 private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();
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 */
58 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
59 Object cookie);
60 }
61
Hall Liu9ccc43d2015-12-15 14:18:33 -080062 /**
63 * Interface to enable stubbing of the call to openInputStream
64 */
65 public interface ContentResolverAdapter {
66 InputStream openInputStream(Context context, Uri uri) throws FileNotFoundException;
67 }
68
Santos Cordon99c8a6f2014-05-28 18:28:47 -070069 // constants
70 private static final int EVENT_LOAD_IMAGE = 1;
71
Santos Cordon99c8a6f2014-05-28 18:28:47 -070072 /** Handler run on a worker thread to load photo asynchronously. */
Ihab Awad8d5d9dd2015-03-12 11:11:06 -070073 private Handler mThreadHandler;
Hall Liu9ccc43d2015-12-15 14:18:33 -080074 private final ContentResolverAdapter mContentResolverAdapter;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070075
Hall Liu9ccc43d2015-12-15 14:18:33 -080076 public ContactsAsyncHelper(ContentResolverAdapter contentResolverAdapter) {
77 mContentResolverAdapter = contentResolverAdapter;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070078 }
79
Santos Cordon99c8a6f2014-05-28 18:28:47 -070080 private static final class WorkerArgs {
81 public Context context;
Makoto Onukia1662d02014-07-10 15:31:59 -070082 public Uri displayPhotoUri;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070083 public Drawable photo;
84 public Bitmap photoIcon;
85 public Object cookie;
86 public OnImageLoadCompleteListener listener;
87 }
88
89 /**
90 * Thread worker class that handles the task of opening the stream and loading
91 * the images.
92 */
Ihab Awad8d5d9dd2015-03-12 11:11:06 -070093 private class WorkerHandler extends Handler {
Santos Cordon99c8a6f2014-05-28 18:28:47 -070094 public WorkerHandler(Looper looper) {
95 super(looper);
96 }
97
98 @Override
99 public void handleMessage(Message msg) {
100 WorkerArgs args = (WorkerArgs) msg.obj;
101
102 switch (msg.arg1) {
103 case EVENT_LOAD_IMAGE:
104 InputStream inputStream = null;
105 try {
106 try {
Hall Liu9ccc43d2015-12-15 14:18:33 -0800107 inputStream = mContentResolverAdapter.openInputStream(
108 args.context, args.displayPhotoUri);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700109 } catch (Exception e) {
110 Log.e(this, e, "Error opening photo input stream");
111 }
112
113 if (inputStream != null) {
114 args.photo = Drawable.createFromStream(inputStream,
Makoto Onukia1662d02014-07-10 15:31:59 -0700115 args.displayPhotoUri.toString());
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700116
117 // This assumes Drawable coming from contact database is usually
118 // BitmapDrawable and thus we can have (down)scaled version of it.
119 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
120
121 Log.d(this, "Loading image: " + msg.arg1 +
Makoto Onukia1662d02014-07-10 15:31:59 -0700122 " token: " + msg.what + " image URI: " + args.displayPhotoUri);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700123 } else {
124 args.photo = null;
125 args.photoIcon = null;
126 Log.d(this, "Problem with image: " + msg.arg1 +
Makoto Onukia1662d02014-07-10 15:31:59 -0700127 " token: " + msg.what + " image URI: " + args.displayPhotoUri +
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700128 ", using default image.");
129 }
130 } finally {
131 if (inputStream != null) {
132 try {
133 inputStream.close();
134 } catch (IOException e) {
135 Log.e(this, e, "Unable to close input stream.");
136 }
137 }
138 }
Ihab Awadabcbce42015-04-07 14:04:01 -0700139
140 // Listener will synchronize as needed
141 Log.d(this, "Notifying listener: " + args.listener.toString() +
142 " image: " + args.displayPhotoUri + " completed");
143 args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
Ihab Awad8d5d9dd2015-03-12 11:11:06 -0700144 args.cookie);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700145 break;
146 default:
Ihab Awad8d5d9dd2015-03-12 11:11:06 -0700147 break;
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700148 }
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700149 }
150
151 /**
152 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
153 * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
154 * create a scaled Bitmap for the Drawable.
155 */
156 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
157 if (!(photo instanceof BitmapDrawable)) {
158 return null;
159 }
160 int iconSize = context.getResources()
161 .getDimensionPixelSize(R.dimen.notification_icon_size);
162 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
163 int orgWidth = orgBitmap.getWidth();
164 int orgHeight = orgBitmap.getHeight();
165 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
166 // We want downscaled one only when the original icon is too big.
167 if (longerEdge > iconSize) {
168 float ratio = ((float) longerEdge) / iconSize;
169 int newWidth = (int) (orgWidth / ratio);
170 int newHeight = (int) (orgHeight / ratio);
171 // If the longer edge is much longer than the shorter edge, the latter may
172 // become 0 which will cause a crash.
173 if (newWidth <= 0 || newHeight <= 0) {
174 Log.w(this, "Photo icon's width or height become 0.");
175 return null;
176 }
177
178 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
179 // should be smaller than the original.
180 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
181 } else {
182 return orgBitmap;
183 }
184 }
185 }
186
187 /**
188 * Starts an asynchronous image load. After finishing the load,
189 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
190 * will be called.
191 *
192 * @param token Arbitrary integer which will be returned as the first argument of
193 * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
194 * @param context Context object used to do the time-consuming operation.
Makoto Onukia1662d02014-07-10 15:31:59 -0700195 * @param displayPhotoUri Uri to be used to fetch the photo
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700196 * @param listener Callback object which will be used when the asynchronous load is done.
197 * Can be null, which means only the asynchronous load is done while there's no way to
198 * obtain the loaded photos.
199 * @param cookie Arbitrary object the caller wants to remember, which will become the
200 * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
201 * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
202 */
Hall Liu5b70c1c2016-03-03 18:42:57 -0800203 public void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700204 OnImageLoadCompleteListener listener, Object cookie) {
Ihab Awad8d5d9dd2015-03-12 11:11:06 -0700205 ensureAsyncHandlerStarted();
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700206
207 // in case the source caller info is null, the URI will be null as well.
208 // just update using the placeholder image in this case.
Makoto Onukia1662d02014-07-10 15:31:59 -0700209 if (displayPhotoUri == null) {
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700210 Log.wtf(LOG_TAG, "Uri is missing");
211 return;
212 }
213
214 // Added additional Cookie field in the callee to handle arguments
215 // sent to the callback function.
216
217 // setup arguments
218 WorkerArgs args = new WorkerArgs();
219 args.cookie = cookie;
220 args.context = context;
Makoto Onukia1662d02014-07-10 15:31:59 -0700221 args.displayPhotoUri = displayPhotoUri;
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700222 args.listener = listener;
223
224 // setup message arguments
Ihab Awad8d5d9dd2015-03-12 11:11:06 -0700225 Message msg = mThreadHandler.obtainMessage(token);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700226 msg.arg1 = EVENT_LOAD_IMAGE;
227 msg.obj = args;
228
Makoto Onukia1662d02014-07-10 15:31:59 -0700229 Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700230 ", displaying default image for now.");
231
232 // notify the thread to begin working
Ihab Awad8d5d9dd2015-03-12 11:11:06 -0700233 mThreadHandler.sendMessage(msg);
234 }
235
236 private void ensureAsyncHandlerStarted() {
237 if (mThreadHandler == null) {
238 HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
239 thread.start();
240 mThreadHandler = new WorkerHandler(thread.getLooper());
241 }
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700242 }
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700243}