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