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.InputStream;
     37 
     38 /**
     39  * Helper class for loading contacts photo asynchronously.
     40  */
     41 public 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