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
17package com.android.phone;
18
19import android.app.Notification;
20import android.content.ContentUris;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.graphics.drawable.BitmapDrawable;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.os.Looper;
29import android.os.Message;
30import android.provider.ContactsContract.Contacts;
31import android.util.Log;
32
33import com.android.internal.telephony.CallerInfo;
34import com.android.internal.telephony.Connection;
35
36import java.io.InputStream;
37
38/**
39 * Helper class for loading contacts photo asynchronously.
40 */
41public class ContactsAsyncHelper {
42
43    private static final boolean DBG = false;
44    private static final String LOG_TAG = "ContactsAsyncHelper";
45
46    /**
47     * Interface for a WorkerHandler result return.
48     */
49    public interface OnImageLoadCompleteListener {
50        /**
51         * Called when the image load is complete.
52         *
53         * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
54         * Context, Uri, OnImageLoadCompleteListener, Object)}.
55         * @param photo Drawable object obtained by the async load.
56         * @param photoIcon Bitmap object obtained by the async load.
57         * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
58         * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
59         * cookie is null.
60         */
61        public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
62                Object cookie);
63    }
64
65    // constants
66    private static final int EVENT_LOAD_IMAGE = 1;
67
68    private final Handler mResultHandler = new Handler() {
69        /** Called when loading is done. */
70        @Override
71        public void handleMessage(Message msg) {
72            WorkerArgs args = (WorkerArgs) msg.obj;
73            switch (msg.arg1) {
74                case EVENT_LOAD_IMAGE:
75                    if (args.listener != null) {
76                        if (DBG) {
77                            Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
78                                    " image: " + args.uri + " completed");
79                        }
80                        args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
81                                args.cookie);
82                    }
83                    break;
84                default:
85            }
86        }
87    };
88
89    /** Handler run on a worker thread to load photo asynchronously. */
90    private static Handler sThreadHandler;
91
92    /** For forcing the system to call its constructor */
93    @SuppressWarnings("unused")
94    private static ContactsAsyncHelper sInstance;
95
96    static {
97        sInstance = new ContactsAsyncHelper();
98    }
99
100    private static final class WorkerArgs {
101        public Context context;
102        public Uri uri;
103        public Drawable photo;
104        public Bitmap photoIcon;
105        public Object cookie;
106        public OnImageLoadCompleteListener listener;
107    }
108
109    /**
110     * public inner class to help out the ContactsAsyncHelper callers
111     * with tracking the state of the CallerInfo Queries and image
112     * loading.
113     *
114     * Logic contained herein is used to remove the race conditions
115     * that exist as the CallerInfo queries run and mix with the image
116     * loads, which then mix with the Phone state changes.
117     */
118    public static class ImageTracker {
119
120        // Image display states
121        public static final int DISPLAY_UNDEFINED = 0;
122        public static final int DISPLAY_IMAGE = -1;
123        public static final int DISPLAY_DEFAULT = -2;
124
125        // State of the image on the imageview.
126        private CallerInfo mCurrentCallerInfo;
127        private int displayMode;
128
129        public ImageTracker() {
130            mCurrentCallerInfo = null;
131            displayMode = DISPLAY_UNDEFINED;
132        }
133
134        /**
135         * Used to see if the requested call / connection has a
136         * different caller attached to it than the one we currently
137         * have in the CallCard.
138         */
139        public boolean isDifferentImageRequest(CallerInfo ci) {
140            // note, since the connections are around for the lifetime of the
141            // call, and the CallerInfo-related items as well, we can
142            // definitely use a simple != comparison.
143            return (mCurrentCallerInfo != ci);
144        }
145
146        public boolean isDifferentImageRequest(Connection connection) {
147            // if the connection does not exist, see if the
148            // mCurrentCallerInfo is also null to match.
149            if (connection == null) {
150                if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
151                return (mCurrentCallerInfo != null);
152            }
153            Object o = connection.getUserData();
154
155            // if the call does NOT have a callerInfo attached
156            // then it is ok to query.
157            boolean runQuery = true;
158            if (o instanceof CallerInfo) {
159                runQuery = isDifferentImageRequest((CallerInfo) o);
160            }
161            return runQuery;
162        }
163
164        /**
165         * Simple setter for the CallerInfo object.
166         */
167        public void setPhotoRequest(CallerInfo ci) {
168            mCurrentCallerInfo = ci;
169        }
170
171        /**
172         * Convenience method used to retrieve the URI
173         * representing the Photo file recorded in the attached
174         * CallerInfo Object.
175         */
176        public Uri getPhotoUri() {
177            if (mCurrentCallerInfo != null) {
178                return ContentUris.withAppendedId(Contacts.CONTENT_URI,
179                        mCurrentCallerInfo.person_id);
180            }
181            return null;
182        }
183
184        /**
185         * Simple setter for the Photo state.
186         */
187        public void setPhotoState(int state) {
188            displayMode = state;
189        }
190
191        /**
192         * Simple getter for the Photo state.
193         */
194        public int getPhotoState() {
195            return displayMode;
196        }
197    }
198
199    /**
200     * Thread worker class that handles the task of opening the stream and loading
201     * the images.
202     */
203    private class WorkerHandler extends Handler {
204        public WorkerHandler(Looper looper) {
205            super(looper);
206        }
207
208        @Override
209        public void handleMessage(Message msg) {
210            WorkerArgs args = (WorkerArgs) msg.obj;
211
212            switch (msg.arg1) {
213                case EVENT_LOAD_IMAGE:
214                    InputStream inputStream = null;
215                    try {
216                        inputStream = Contacts.openContactPhotoInputStream(
217                                args.context.getContentResolver(), args.uri, true);
218                    } catch (Exception e) {
219                        Log.e(LOG_TAG, "Error opening photo input stream", e);
220                    }
221
222                    if (inputStream != null) {
223                        args.photo = Drawable.createFromStream(inputStream, args.uri.toString());
224
225                        // This assumes Drawable coming from contact database is usually
226                        // BitmapDrawable and thus we can have (down)scaled version of it.
227                        args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
228
229                        if (DBG) Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
230                                " token: " + msg.what + " image URI: " + args.uri);
231                    } else {
232                        args.photo = null;
233                        args.photoIcon = null;
234                        if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
235                                " token: " + msg.what + " image URI: " + args.uri +
236                                ", using default image.");
237                    }
238                    break;
239                default:
240            }
241
242            // send the reply to the enclosing class.
243            Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
244            reply.arg1 = msg.arg1;
245            reply.obj = msg.obj;
246            reply.sendToTarget();
247        }
248
249        /**
250         * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
251         * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
252         * create a scaled Bitmap for the Drawable.
253         */
254        private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
255            if (!(photo instanceof BitmapDrawable)) {
256                return null;
257            }
258            int iconSize = context.getResources()
259                    .getDimensionPixelSize(R.dimen.notification_icon_size);
260            Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
261            int orgWidth = orgBitmap.getWidth();
262            int orgHeight = orgBitmap.getHeight();
263            int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
264            // We want downscaled one only when the original icon is too big.
265            if (longerEdge > iconSize) {
266                float ratio = ((float) longerEdge) / iconSize;
267                int newWidth = (int) (orgWidth / ratio);
268                int newHeight = (int) (orgHeight / ratio);
269                // If the longer edge is much longer than the shorter edge, the latter may
270                // become 0 which will cause a crash.
271                if (newWidth <= 0 || newHeight <= 0) {
272                    Log.w(LOG_TAG, "Photo icon's width or height become 0.");
273                    return null;
274                }
275
276                // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
277                // should be smaller than the original.
278                return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
279            } else {
280                return orgBitmap;
281            }
282        }
283    }
284
285    /**
286     * Private constructor for static class
287     */
288    private ContactsAsyncHelper() {
289        HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
290        thread.start();
291        sThreadHandler = new WorkerHandler(thread.getLooper());
292    }
293
294    /**
295     * Starts an asynchronous image load. After finishing the load,
296     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
297     * will be called.
298     *
299     * @param token Arbitrary integer which will be returned as the first argument of
300     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
301     * @param context Context object used to do the time-consuming operation.
302     * @param personUri Uri to be used to fetch the photo
303     * @param listener Callback object which will be used when the asynchronous load is done.
304     * Can be null, which means only the asynchronous load is done while there's no way to
305     * obtain the loaded photos.
306     * @param cookie Arbitrary object the caller wants to remember, which will become the
307     * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
308     * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
309     */
310    public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
311            OnImageLoadCompleteListener listener, Object cookie) {
312        // in case the source caller info is null, the URI will be null as well.
313        // just update using the placeholder image in this case.
314        if (personUri == null) {
315            Log.wtf(LOG_TAG, "Uri is missing");
316            return;
317        }
318
319        // Added additional Cookie field in the callee to handle arguments
320        // sent to the callback function.
321
322        // setup arguments
323        WorkerArgs args = new WorkerArgs();
324        args.cookie = cookie;
325        args.context = context;
326        args.uri = personUri;
327        args.listener = listener;
328
329        // setup message arguments
330        Message msg = sThreadHandler.obtainMessage(token);
331        msg.arg1 = EVENT_LOAD_IMAGE;
332        msg.obj = args;
333
334        if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
335                ", displaying default image for now.");
336
337        // notify the thread to begin working
338        sThreadHandler.sendMessage(msg);
339    }
340
341
342}
343