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.content.res.AssetFileDescriptor;
     23 import android.graphics.Bitmap;
     24 import android.graphics.drawable.BitmapDrawable;
     25 import android.graphics.drawable.Drawable;
     26 import android.net.Uri;
     27 import android.os.Handler;
     28 import android.os.HandlerThread;
     29 import android.os.Looper;
     30 import android.os.Message;
     31 import android.provider.ContactsContract.Contacts;
     32 
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 
     36 /**
     37  * Helper class for loading contacts photo asynchronously.
     38  */
     39 public class ContactsAsyncHelper {
     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 final Handler mResultHandler = new Handler() {
     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 Handler sThreadHandler;
     84 
     85     /** For forcing the system to call its constructor */
     86     @SuppressWarnings("unused")
     87     private static ContactsAsyncHelper sInstance;
     88 
     89     static {
     90         sInstance = new ContactsAsyncHelper();
     91     }
     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 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, "Error opening photo input stream", e);
    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(ContactsAsyncHelper.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(ContactsAsyncHelper.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, "Unable to close input stream.", e);
    149                             }
    150                         }
    151                     }
    152                     break;
    153                 default:
    154             }
    155 
    156             // send the reply to the enclosing class.
    157             Message reply = ContactsAsyncHelper.this.mResultHandler.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      * Private constructor for static class
    201      */
    202     private ContactsAsyncHelper() {
    203         HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
    204         thread.start();
    205         sThreadHandler = new WorkerHandler(thread.getLooper());
    206     }
    207 
    208     /**
    209      * Starts an asynchronous image load. After finishing the load,
    210      * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
    211      * will be called.
    212      *
    213      * @param token Arbitrary integer which will be returned as the first argument of
    214      * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
    215      * @param context Context object used to do the time-consuming operation.
    216      * @param displayPhotoUri Uri to be used to fetch the photo
    217      * @param listener Callback object which will be used when the asynchronous load is done.
    218      * Can be null, which means only the asynchronous load is done while there's no way to
    219      * obtain the loaded photos.
    220      * @param cookie Arbitrary object the caller wants to remember, which will become the
    221      * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
    222      * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
    223      */
    224     public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
    225             OnImageLoadCompleteListener listener, Object cookie) {
    226         // in case the source caller info is null, the URI will be null as well.
    227         // just update using the placeholder image in this case.
    228         if (displayPhotoUri == null) {
    229             Log.wtf("startObjectPhotoAsync", "Uri is missing");
    230             return;
    231         }
    232 
    233         // Added additional Cookie field in the callee to handle arguments
    234         // sent to the callback function.
    235 
    236         // setup arguments
    237         WorkerArgs args = new WorkerArgs();
    238         args.cookie = cookie;
    239         args.context = context;
    240         args.displayPhotoUri = displayPhotoUri;
    241         args.listener = listener;
    242 
    243         // setup message arguments
    244         Message msg = sThreadHandler.obtainMessage(token);
    245         msg.arg1 = EVENT_LOAD_IMAGE;
    246         msg.obj = args;
    247 
    248         Log.d("startObjectPhotoAsync", "Begin loading image: " + args.displayPhotoUri +
    249                 ", displaying default image for now.");
    250 
    251         // notify the thread to begin working
    252         sThreadHandler.sendMessage(msg);
    253     }
    254 
    255 
    256 }
    257