Home | History | Annotate | Download | only in bitmap
      1 /*
      2  * Copyright (C) 2013 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.bitmap;
     19 
     20 import android.content.ContentResolver;
     21 import android.os.AsyncTask;
     22 import android.os.AsyncTask.Status;
     23 import android.os.Handler;
     24 
     25 import com.android.bitmap.BitmapCache;
     26 import com.android.bitmap.DecodeTask;
     27 import com.android.bitmap.RequestKey;
     28 import com.android.bitmap.ReusableBitmap;
     29 import com.android.ex.photo.util.Trace;
     30 import com.android.mail.ContactInfo;
     31 import com.android.mail.SenderInfoLoader;
     32 import com.android.mail.bitmap.ContactRequest.ContactRequestHolder;
     33 import com.android.mail.utils.LogTag;
     34 import com.android.mail.utils.LogUtils;
     35 import com.google.common.collect.ImmutableMap;
     36 
     37 import java.util.HashSet;
     38 import java.util.LinkedHashSet;
     39 import java.util.Set;
     40 import java.util.concurrent.Executor;
     41 import java.util.concurrent.LinkedBlockingQueue;
     42 import java.util.concurrent.ThreadPoolExecutor;
     43 import java.util.concurrent.TimeUnit;
     44 
     45 /**
     46  * Batches up ContactRequests so we can efficiently query the contacts provider. Kicks off a
     47  * ContactResolverTask to query for contact images in the background.
     48  */
     49 public class ContactResolver implements Runnable {
     50 
     51     private static final String TAG = LogTag.getLogTag();
     52 
     53     // The maximum size returned from ContactsContract.Contacts.Photo.PHOTO is 96px by 96px.
     54     private static final int MAXIMUM_PHOTO_SIZE = 96;
     55     private static final int HALF_MAXIMUM_PHOTO_SIZE = 48;
     56 
     57     protected final ContentResolver mResolver;
     58     private final BitmapCache mCache;
     59     /** Insertion ordered set allows us to work from the top down. */
     60     private final LinkedHashSet<ContactRequestHolder> mBatch;
     61 
     62     private final Handler mHandler = new Handler();
     63     private ContactResolverTask mTask;
     64 
     65 
     66     /** Size 1 pool mostly to make systrace output traces on one line. */
     67     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
     68             1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
     69     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
     70 
     71     public interface ContactDrawableInterface {
     72         public void onDecodeComplete(final RequestKey key, final ReusableBitmap result);
     73         public int getDecodeWidth();
     74         public int getDecodeHeight();
     75     }
     76 
     77     public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
     78         mResolver = resolver;
     79         mCache = cache;
     80         mBatch = new LinkedHashSet<ContactRequestHolder>();
     81     }
     82 
     83     @Override
     84     public void run() {
     85         // Start to process a new batch.
     86         if (mBatch.isEmpty()) {
     87             return;
     88         }
     89 
     90         if (mTask != null && mTask.getStatus() == Status.RUNNING) {
     91             LogUtils.d(TAG, "ContactResolver << batch skip");
     92             return;
     93         }
     94 
     95         Trace.beginSection("ContactResolver run");
     96         LogUtils.d(TAG, "ContactResolver >> batch start");
     97 
     98         // Make a copy of the batch.
     99         LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
    100 
    101         if (mTask != null) {
    102             mTask.cancel(true);
    103         }
    104 
    105         mTask = getContactResolverTask(batch);
    106         mTask.executeOnExecutor(EXECUTOR);
    107         Trace.endSection();
    108     }
    109 
    110     protected ContactResolverTask getContactResolverTask(
    111             LinkedHashSet<ContactRequestHolder> batch) {
    112         return new ContactResolverTask(batch, mResolver, mCache, this);
    113     }
    114 
    115     public BitmapCache getCache() {
    116         return mCache;
    117     }
    118 
    119     public void add(final ContactRequest request, final ContactDrawableInterface drawable) {
    120         mBatch.add(new ContactRequestHolder(request, drawable));
    121         notifyBatchReady();
    122     }
    123 
    124     public void remove(final ContactRequest request, final ContactDrawableInterface drawable) {
    125         mBatch.remove(new ContactRequestHolder(request, drawable));
    126     }
    127 
    128     /**
    129      * A layout pass traverses the whole tree during a single iteration of the event loop. That
    130      * means that every ContactDrawable on the screen will add its ContactRequest to the batch in
    131      * a single iteration of the event loop.
    132      *
    133      * <p/>
    134      * We take advantage of this by posting a Runnable (happens to be this object) at the end of
    135      * the event queue. Every time something is added to the batch as part of the same layout pass,
    136      * the Runnable is moved to the back of the queue. When the next layout pass occurs,
    137      * it is placed in the event loop behind this Runnable. That allows us to process the batch
    138      * that was added previously.
    139      */
    140     private void notifyBatchReady() {
    141         LogUtils.d(TAG, "ContactResolver  > batch   %d", mBatch.size());
    142         mHandler.removeCallbacks(this);
    143         mHandler.post(this);
    144     }
    145 
    146     /**
    147      * This is not a very traditional AsyncTask, in the sense that we do not care about what gets
    148      * returned in doInBackground(). Instead, we signal traditional "return values" through
    149      * publishProgress().
    150      *
    151      * <p/>
    152      * The reason we do this is because this task is responsible for decoding an entire batch of
    153      * ContactRequests. But, we do not want to have to wait to decode all of them before updating
    154      * any views. So we must do all the work in doInBackground(),
    155      * but upon finishing each individual task, we need to jump out to the UI thread and update
    156      * that view.
    157      */
    158     public static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
    159 
    160         private final Set<ContactRequestHolder> mContactRequests;
    161         private final ContentResolver mResolver;
    162         private final BitmapCache mCache;
    163         private final ContactResolver mCallback;
    164 
    165         public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
    166                 final ContentResolver resolver, final BitmapCache cache,
    167                 final ContactResolver callback) {
    168             mContactRequests = contactRequests;
    169             mResolver = resolver;
    170             mCache = cache;
    171             mCallback = callback;
    172         }
    173 
    174         @Override
    175         protected Void doInBackground(final Void... params) {
    176             Trace.beginSection("set up");
    177             final Set<String> emails = new HashSet<String>(mContactRequests.size());
    178             for (ContactRequestHolder request : mContactRequests) {
    179                 final String email = request.getEmail();
    180                 emails.add(email);
    181             }
    182             Trace.endSection();
    183 
    184             Trace.beginSection("load contact photo bytes");
    185             // Query the contacts provider for the current batch of emails.
    186             final ImmutableMap<String, ContactInfo> contactInfos = loadContactPhotos(emails);
    187             Trace.endSection();
    188 
    189             for (ContactRequestHolder request : mContactRequests) {
    190                 Trace.beginSection("decode");
    191                 final String email = request.getEmail();
    192                 if (contactInfos == null) {
    193                     // Query failed.
    194                     LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
    195                     publishProgress(new Result(request, null));
    196                     Trace.endSection();
    197                     continue;
    198                 }
    199 
    200                 final ContactInfo contactInfo = contactInfos.get(email);
    201                 if (contactInfo == null) {
    202                     // Request skipped. Try again next batch.
    203                     LogUtils.d(TAG, "ContactResolver  = skipped %s", email);
    204                     Trace.endSection();
    205                     continue;
    206                 }
    207 
    208                 // Query attempted.
    209                 final byte[] photo = contactInfo.photoBytes;
    210                 if (photo == null) {
    211                     // No photo bytes found.
    212                     LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
    213                     publishProgress(new Result(request, null));
    214                     Trace.endSection();
    215                     continue;
    216                 }
    217 
    218                 // Query succeeded. Photo bytes found.
    219                 request.contactRequest.bytes = photo;
    220 
    221                 // Start decode.
    222                 LogUtils.d(TAG, "ContactResolver ++ found   %s", email);
    223                 // Synchronously decode the photo bytes. We are already in a background
    224                 // thread, and we want decodes to finish in order. The decodes are blazing
    225                 // fast so we don't need to kick off multiple threads.
    226                 final int width = HALF_MAXIMUM_PHOTO_SIZE >= request.destination.getDecodeWidth()
    227                         ? HALF_MAXIMUM_PHOTO_SIZE : MAXIMUM_PHOTO_SIZE;
    228                 final int height = HALF_MAXIMUM_PHOTO_SIZE >= request.destination.getDecodeHeight()
    229                         ? HALF_MAXIMUM_PHOTO_SIZE : MAXIMUM_PHOTO_SIZE;
    230                 final DecodeTask.DecodeOptions opts = new DecodeTask.DecodeOptions(
    231                         width, height, 1 / 2f, DecodeTask.DecodeOptions.STRATEGY_ROUND_NEAREST);
    232                 final ReusableBitmap result = new DecodeTask(request.contactRequest, opts, null,
    233                         null, mCache).decode();
    234                 request.contactRequest.bytes = null;
    235 
    236                 // Decode success.
    237                 publishProgress(new Result(request, result));
    238                 Trace.endSection();
    239             }
    240 
    241             return null;
    242         }
    243 
    244         protected ImmutableMap<String, ContactInfo> loadContactPhotos(Set<String> emails) {
    245             if (mResolver == null) {
    246                 return null;
    247             }
    248             return SenderInfoLoader.loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
    249         }
    250 
    251         /**
    252          * We use progress updates to jump to the UI thread so we can decode the batch
    253          * incrementally.
    254          */
    255         @Override
    256         protected void onProgressUpdate(final Result... values) {
    257             final ContactRequestHolder request = values[0].request;
    258             final ReusableBitmap bitmap = values[0].bitmap;
    259 
    260             // DecodeTask does not add null results to the cache.
    261             if (bitmap == null && mCache != null) {
    262                 // Cache null result.
    263                 mCache.put(request.contactRequest, null);
    264             }
    265 
    266             request.destination.onDecodeComplete(request.contactRequest, bitmap);
    267         }
    268 
    269         @Override
    270         protected void onPostExecute(final Void aVoid) {
    271             // Batch completed. Start next batch.
    272             mCallback.notifyBatchReady();
    273         }
    274     }
    275 
    276     /**
    277      * Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
    278      * to onProgressUpdate().
    279      */
    280     private static class Result {
    281         public final ContactRequestHolder request;
    282         public final ReusableBitmap bitmap;
    283 
    284         private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
    285             this.request = request;
    286             this.bitmap = bitmap;
    287         }
    288     }
    289 }
    290