blob: abca9246ece4870a7ad7b4181dc790b793d68642 [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
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
17package com.android.incallui;
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;
Eric Erfanianccca3152017-02-22 16:32:36 -080025import android.support.annotation.MainThread;
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070026import android.support.annotation.Nullable;
Eric Erfanianccca3152017-02-22 16:32:36 -080027import android.support.annotation.WorkerThread;
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070028import com.android.dialer.common.LogUtil;
29import com.android.dialer.common.concurrent.DialerExecutor;
zachh0cd36a62017-10-31 12:04:05 -070030import com.android.dialer.common.concurrent.DialerExecutorComponent;
Eric Erfanianccca3152017-02-22 16:32:36 -080031import java.io.IOException;
32import java.io.InputStream;
33
34/** Helper class for loading contacts photo asynchronously. */
35public class ContactsAsyncHelper {
36
37 /** Interface for a WorkerHandler result return. */
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070038 interface OnImageLoadCompleteListener {
Eric Erfanianccca3152017-02-22 16:32:36 -080039
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
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
55 void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
56 }
57
Eric Erfanianccca3152017-02-22 16:32:36 -080058 /**
59 * Starts an asynchronous image load. After finishing the load, {@link
60 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called.
61 *
62 * @param token Arbitrary integer which will be returned as the first argument of {@link
63 * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
64 * @param context Context object used to do the time-consuming operation.
65 * @param displayPhotoUri Uri to be used to fetch the photo
66 * @param listener Callback object which will be used when the asynchronous load is done. Can be
67 * null, which means only the asynchronous load is done while there's no way to obtain the
68 * loaded photos.
69 * @param cookie Arbitrary object the caller wants to remember, which will become the fourth
70 * argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap,
71 * Object)}. Can be null, at which the callback will also has null for the argument.
72 */
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070073 static void startObtainPhotoAsync(
Eric Erfanianccca3152017-02-22 16:32:36 -080074 int token,
75 Context context,
76 Uri displayPhotoUri,
77 OnImageLoadCompleteListener listener,
78 Object cookie) {
79 // in case the source caller info is null, the URI will be null as well.
80 // just update using the placeholder image in this case.
81 if (displayPhotoUri == null) {
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070082 LogUtil.e("ContactsAsyncHelper.startObjectPhotoAsync", "uri is missing");
Eric Erfanianccca3152017-02-22 16:32:36 -080083 return;
84 }
85
86 // Added additional Cookie field in the callee to handle arguments
87 // sent to the callback function.
88
89 // setup arguments
90 WorkerArgs args = new WorkerArgs();
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070091 args.token = token;
Eric Erfanianccca3152017-02-22 16:32:36 -080092 args.cookie = cookie;
93 args.context = context;
94 args.displayPhotoUri = displayPhotoUri;
95 args.listener = listener;
96
zachh0cd36a62017-10-31 12:04:05 -070097 DialerExecutorComponent.get(context)
98 .dialerExecutorFactory()
99 .createNonUiTaskBuilder(new Worker())
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700100 .onSuccess(
101 output -> {
102 if (args.listener != null) {
103 LogUtil.d(
104 "ContactsAsyncHelper.startObtainPhotoAsync",
105 "notifying listener: "
106 + args.listener
107 + " image: "
108 + args.displayPhotoUri
109 + " completed");
110 args.listener.onImageLoadComplete(
111 args.token, args.photo, args.photoIcon, args.cookie);
112 }
113 })
114 .build()
115 .executeParallel(args);
Eric Erfanianccca3152017-02-22 16:32:36 -0800116 }
117
118 private static final class WorkerArgs {
119
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700120 public int token;
Eric Erfanianccca3152017-02-22 16:32:36 -0800121 public Context context;
122 public Uri displayPhotoUri;
123 public Drawable photo;
124 public Bitmap photoIcon;
125 public Object cookie;
126 public OnImageLoadCompleteListener listener;
127 }
128
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700129 private static class Worker implements DialerExecutor.Worker<WorkerArgs, Void> {
Eric Erfanianccca3152017-02-22 16:32:36 -0800130
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700131 @Nullable
Eric Erfanianccca3152017-02-22 16:32:36 -0800132 @Override
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700133 public Void doInBackground(WorkerArgs args) throws Throwable {
134 InputStream inputStream = null;
135 try {
136 try {
137 inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri);
138 } catch (Exception e) {
139 LogUtil.e(
140 "ContactsAsyncHelper.Worker.doInBackground", "error opening photo input stream", e);
141 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800142
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700143 if (inputStream != null) {
144 args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString());
145
146 // This assumes Drawable coming from contact database is usually
147 // BitmapDrawable and thus we can have (down)scaled version of it.
148 args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
149
150 LogUtil.d(
151 "ContactsAsyncHelper.Worker.doInBackground",
152 "loading image, URI: %s",
153 args.displayPhotoUri);
154 } else {
155 args.photo = null;
156 args.photoIcon = null;
157 LogUtil.d(
158 "ContactsAsyncHelper.Worker.doInBackground",
159 "problem with image, URI: %s, using default image.",
160 args.displayPhotoUri);
161 }
162 if (args.listener != null) {
163 args.listener.onImageLoaded(args.token, args.photo, args.photoIcon, args.cookie);
164 }
165 } finally {
166 if (inputStream != null) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800167 try {
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700168 inputStream.close();
169 } catch (IOException e) {
170 LogUtil.e(
171 "ContactsAsyncHelper.Worker.doInBackground", "Unable to close input stream.", e);
Eric Erfanianccca3152017-02-22 16:32:36 -0800172 }
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700173 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800174 }
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700175 return null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800176 }
177
178 /**
179 * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return
180 * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled
181 * Bitmap for the Drawable.
182 */
183 private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
184 if (!(photo instanceof BitmapDrawable)) {
185 return null;
186 }
187 int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size);
188 Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
189 int orgWidth = orgBitmap.getWidth();
190 int orgHeight = orgBitmap.getHeight();
191 int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
192 // We want downscaled one only when the original icon is too big.
193 if (longerEdge > iconSize) {
194 float ratio = ((float) longerEdge) / iconSize;
195 int newWidth = (int) (orgWidth / ratio);
196 int newHeight = (int) (orgHeight / ratio);
197 // If the longer edge is much longer than the shorter edge, the latter may
198 // become 0 which will cause a crash.
199 if (newWidth <= 0 || newHeight <= 0) {
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700200 LogUtil.w(
201 "ContactsAsyncHelper.Worker.getPhotoIconWhenAppropriate",
202 "Photo icon's width or height become 0.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800203 return null;
204 }
205
206 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
207 // should be smaller than the original.
208 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
209 } else {
210 return orgBitmap;
211 }
212 }
213 }
214}