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