1 /*
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 
17 package com.android.incallui;
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.support.annotation.MainThread;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.WorkerThread;
28 import com.android.dialer.common.LogUtil;
29 import com.android.dialer.common.concurrent.DialerExecutor;
30 import com.android.dialer.common.concurrent.DialerExecutorComponent;
31 import java.io.IOException;
32 import java.io.InputStream;
33 
34 /** Helper class for loading contacts photo asynchronously. */
35 public class ContactsAsyncHelper {
36 
37   /** Interface for a WorkerHandler result return. */
38   interface OnImageLoadCompleteListener {
39 
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
onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)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
onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie)55     void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
56   }
57 
58   /**
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    */
startObtainPhotoAsync( int token, Context context, Uri displayPhotoUri, OnImageLoadCompleteListener listener, Object cookie)73   static void startObtainPhotoAsync(
74       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) {
82       LogUtil.e("ContactsAsyncHelper.startObjectPhotoAsync", "uri is missing");
83       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();
91     args.token = token;
92     args.cookie = cookie;
93     args.context = context;
94     args.displayPhotoUri = displayPhotoUri;
95     args.listener = listener;
96 
97     DialerExecutorComponent.get(context)
98         .dialerExecutorFactory()
99         .createNonUiTaskBuilder(new Worker())
100         .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);
116   }
117 
118   private static final class WorkerArgs {
119 
120     public int token;
121     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 
129   private static class Worker implements DialerExecutor.Worker<WorkerArgs, Void> {
130 
131     @Nullable
132     @Override
doInBackground(WorkerArgs args)133     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         }
142 
143         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) {
167           try {
168             inputStream.close();
169           } catch (IOException e) {
170             LogUtil.e(
171                 "ContactsAsyncHelper.Worker.doInBackground", "Unable to close input stream.", e);
172           }
173         }
174       }
175       return null;
176     }
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      */
getPhotoIconWhenAppropriate(Context context, Drawable photo)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) {
200           LogUtil.w(
201               "ContactsAsyncHelper.Worker.getPhotoIconWhenAppropriate",
202               "Photo icon's width or height become 0.");
203           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 }
215