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