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