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