Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright (C) 2014 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.server.telecom;
     18 
     19 import android.app.Notification;
     20 import android.content.Context;
     21 import android.graphics.Bitmap;
     22 import android.graphics.drawable.BitmapDrawable;
     23 import android.graphics.drawable.Drawable;
     24 import android.net.Uri;
     25 import android.os.Handler;
     26 import android.os.HandlerThread;
     27 import android.os.Looper;
     28 import android.os.Message;
     29 
     30 // TODO: Needed for move to system service: import com.android.internal.R;
     31 
     32 import java.io.FileNotFoundException;
     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     private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();
     41 
     42     /**
     43      * Interface for a WorkerHandler result return.
     44      */
     45     public interface OnImageLoadCompleteListener {
     46         /**
     47          * Called when the image load is complete.
     48          *
     49          * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
     50          * Context, Uri, OnImageLoadCompleteListener, Object)}.
     51          * @param photo Drawable object obtained by the async load.
     52          * @param photoIcon Bitmap object obtained by the async load.
     53          * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
     54          * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
     55          * cookie is null.
     56          */
     57         public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
     58                 Object cookie);
     59     }
     60 
     61     /**
     62      * Interface to enable stubbing of the call to openInputStream
     63      */
     64     public interface ContentResolverAdapter {
     65         InputStream openInputStream(Context context, Uri uri) throws FileNotFoundException;
     66     }
     67 
     68     // constants
     69     private static final int EVENT_LOAD_IMAGE = 1;
     70 
     71     /** Handler run on a worker thread to load photo asynchronously. */
     72     private Handler mThreadHandler;
     73     private final ContentResolverAdapter mContentResolverAdapter;
     74 
     75     public ContactsAsyncHelper(ContentResolverAdapter contentResolverAdapter) {
     76         mContentResolverAdapter = contentResolverAdapter;
     77     }
     78 
     79     private static final class WorkerArgs {
     80         public Context context;
     81         public Uri displayPhotoUri;
     82         public Drawable photo;
     83         public Bitmap photoIcon;
     84         public Object cookie;
     85         public OnImageLoadCompleteListener listener;
     86     }
     87 
     88     /**
     89      * Thread worker class that handles the task of opening the stream and loading
     90      * the images.
     91      */
     92     private class WorkerHandler extends Handler {
     93         public WorkerHandler(Looper looper) {
     94             super(looper);
     95         }
     96 
     97         @Override
     98         public void handleMessage(Message msg) {
     99             WorkerArgs args = (WorkerArgs) msg.obj;
    100 
    101             switch (msg.arg1) {
    102                 case EVENT_LOAD_IMAGE:
    103                     InputStream inputStream = null;
    104                     try {
    105                         try {
    106                             inputStream = mContentResolverAdapter.openInputStream(
    107                                     args.context, args.displayPhotoUri);
    108                         } catch (Exception e) {
    109                             Log.e(this, e, "Error opening photo input stream");
    110                         }
    111 
    112                         if (inputStream != null) {
    113                             args.photo = Drawable.createFromStream(inputStream,
    114                                     args.displayPhotoUri.toString());
    115 
    116                             // This assumes Drawable coming from contact database is usually
    117                             // BitmapDrawable and thus we can have (down)scaled version of it.
    118                             args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
    119 
    120                             Log.d(this, "Loading image: " + msg.arg1 +
    121                                     " token: " + msg.what + " image URI: " + args.displayPhotoUri);
    122                         } else {
    123                             args.photo = null;
    124                             args.photoIcon = null;
    125                             Log.d(this, "Problem with image: " + msg.arg1 +
    126                                     " token: " + msg.what + " image URI: " + args.displayPhotoUri +
    127                                     ", using default image.");
    128                         }
    129                     } finally {
    130                         if (inputStream != null) {
    131                             try {
    132                                 inputStream.close();
    133                             } catch (IOException e) {
    134                                 Log.e(this, e, "Unable to close input stream.");
    135                             }
    136                         }
    137                     }
    138 
    139                     // Listener will synchronize as needed
    140                     Log.d(this, "Notifying listener: " + args.listener.toString() +
    141                             " image: " + args.displayPhotoUri + " completed");
    142                     args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
    143                                 args.cookie);
    144                     break;
    145                 default:
    146                     break;
    147             }
    148         }
    149 
    150         /**
    151          * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
    152          * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
    153          * create a scaled Bitmap for the Drawable.
    154          */
    155         private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
    156             if (!(photo instanceof BitmapDrawable)) {
    157                 return null;
    158             }
    159             int iconSize = context.getResources()
    160                     .getDimensionPixelSize(R.dimen.notification_icon_size);
    161             Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
    162             int orgWidth = orgBitmap.getWidth();
    163             int orgHeight = orgBitmap.getHeight();
    164             int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
    165             // We want downscaled one only when the original icon is too big.
    166             if (longerEdge > iconSize) {
    167                 float ratio = ((float) longerEdge) / iconSize;
    168                 int newWidth = (int) (orgWidth / ratio);
    169                 int newHeight = (int) (orgHeight / ratio);
    170                 // If the longer edge is much longer than the shorter edge, the latter may
    171                 // become 0 which will cause a crash.
    172                 if (newWidth <= 0 || newHeight <= 0) {
    173                     Log.w(this, "Photo icon's width or height become 0.");
    174                     return null;
    175                 }
    176 
    177                 // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
    178                 // should be smaller than the original.
    179                 return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
    180             } else {
    181                 return orgBitmap;
    182             }
    183         }
    184     }
    185 
    186     /**
    187      * Starts an asynchronous image load. After finishing the load,
    188      * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
    189      * will be called.
    190      *
    191      * @param token Arbitrary integer which will be returned as the first argument of
    192      * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
    193      * @param context Context object used to do the time-consuming operation.
    194      * @param displayPhotoUri Uri to be used to fetch the photo
    195      * @param listener Callback object which will be used when the asynchronous load is done.
    196      * Can be null, which means only the asynchronous load is done while there's no way to
    197      * obtain the loaded photos.
    198      * @param cookie Arbitrary object the caller wants to remember, which will become the
    199      * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
    200      * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
    201      */
    202     public void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
    203             OnImageLoadCompleteListener listener, Object cookie) {
    204         ensureAsyncHandlerStarted();
    205 
    206         // in case the source caller info is null, the URI will be null as well.
    207         // just update using the placeholder image in this case.
    208         if (displayPhotoUri == null) {
    209             Log.wtf(LOG_TAG, "Uri is missing");
    210             return;
    211         }
    212 
    213         // Added additional Cookie field in the callee to handle arguments
    214         // sent to the callback function.
    215 
    216         // setup arguments
    217         WorkerArgs args = new WorkerArgs();
    218         args.cookie = cookie;
    219         args.context = context;
    220         args.displayPhotoUri = displayPhotoUri;
    221         args.listener = listener;
    222 
    223         // setup message arguments
    224         Message msg = mThreadHandler.obtainMessage(token);
    225         msg.arg1 = EVENT_LOAD_IMAGE;
    226         msg.obj = args;
    227 
    228         Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
    229                 ", displaying default image for now.");
    230 
    231         // notify the thread to begin working
    232         mThreadHandler.sendMessage(msg);
    233     }
    234 
    235     private void ensureAsyncHandlerStarted() {
    236         if (mThreadHandler == null) {
    237             HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
    238             thread.start();
    239             mThreadHandler = new WorkerHandler(thread.getLooper());
    240         }
    241     }
    242 }
    243