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     protected final ContentResolver mResolver;
     54     private final BitmapCache mCache;
     55     /** Insertion ordered set allows us to work from the top down. */
     56     private final LinkedHashSet<ContactRequestHolder> mBatch;
     57 
     58     private final Handler mHandler = new Handler();
     59     private ContactResolverTask mTask;
     60 
     61 
     62     /** Size 1 pool mostly to make systrace output traces on one line. */
     63     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
     64             1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
     65     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
     66 
     67     public interface ContactDrawableInterface {
     68         public void onDecodeComplete(final RequestKey key, final ReusableBitmap result);
     69         public int getDecodeWidth();
     70         public int getDecodeHeight();
     71     }
     72 
     73     public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
     74         mResolver = resolver;
     75         mCache = cache;
     76         mBatch = new LinkedHashSet<ContactRequestHolder>();
     77     }
     78 
     79     @Override
     80     public void run() {
     81         // Start to process a new batch.
     82         if (mBatch.isEmpty()) {
     83             return;
     84         }
     85 
     86         if (mTask != null && mTask.getStatus() == Status.RUNNING) {
     87             LogUtils.d(TAG, "ContactResolver << batch skip");
     88             return;
     89         }
     90 
     91         Trace.beginSection("ContactResolver run");
     92         LogUtils.d(TAG, "ContactResolver >> batch start");
     93 
     94         // Make a copy of the batch.
     95         LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
     96 
     97         if (mTask != null) {
     98             mTask.cancel(true);
     99         }
    100 
    101         mTask = getContactResolverTask(batch);
    102         mTask.executeOnExecutor(EXECUTOR);
    103         Trace.endSection();
    104     }
    105 
    106     protected ContactResolverTask getContactResolverTask(
    107             LinkedHashSet<ContactRequestHolder> batch) {
    108         return new ContactResolverTask(batch, mResolver, mCache, this);
    109     }
    110 
    111     public BitmapCache getCache() {
    112         return mCache;
    113     }
    114 
    115     public void add(final ContactRequest request, final ContactDrawableInterface drawable) {
    116         mBatch.add(new ContactRequestHolder(request, drawable));
    117         notifyBatchReady();
    118     }
    119 
    120     public void remove(final ContactRequest request, final ContactDrawableInterface drawable) {
    121         mBatch.remove(new ContactRequestHolder(request, drawable));
    122     }
    123 
    124     /**
    125      * A layout pass traverses the whole tree during a single iteration of the event loop. That
    126      * means that every ContactDrawable on the screen will add its ContactRequest to the batch in
    127      * a single iteration of the event loop.
    128      *
    129      * <p/>
    130      * We take advantage of this by posting a Runnable (happens to be this object) at the end of
    131      * the event queue. Every time something is added to the batch as part of the same layout pass,
    132      * the Runnable is moved to the back of the queue. When the next layout pass occurs,
    133      * it is placed in the event loop behind this Runnable. That allows us to process the batch
    134      * that was added previously.
    135      */
    136     private void notifyBatchReady() {
    137         LogUtils.d(TAG, "ContactResolver  > batch   %d", mBatch.size());
    138         mHandler.removeCallbacks(this);
    139         mHandler.post(this);
    140     }
    141 
    142     /**
    143      * This is not a very traditional AsyncTask, in the sense that we do not care about what gets
    144      * returned in doInBackground(). Instead, we signal traditional "return values" through
    145      * publishProgress().
    146      *
    147      * <p/>
    148      * The reason we do this is because this task is responsible for decoding an entire batch of
    149      * ContactRequests. But, we do not want to have to wait to decode all of them before updating
    150      * any views. So we must do all the work in doInBackground(),
    151      * but upon finishing each individual task, we need to jump out to the UI thread and update
    152      * that view.
    153      */
    154     public static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
    155 
    156         private final Set<ContactRequestHolder> mContactRequests;
    157         private final ContentResolver mResolver;
    158         private final BitmapCache mCache;
    159         private final ContactResolver mCallback;
    160 
    161         public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
    162                 final ContentResolver resolver, final BitmapCache cache,
    163                 final ContactResolver callback) {
    164             mContactRequests = contactRequests;
    165             mResolver = resolver;
    166             mCache = cache;
    167             mCallback = callback;
    168         }
    169 
    170         @Override
    171         protected Void doInBackground(final Void... params) {
    172             Trace.beginSection("set up");
    173             final Set<String> emails = new HashSet<String>(mContactRequests.size());
    174             for (ContactRequestHolder request : mContactRequests) {
    175                 final String email = request.getEmail();
    176                 emails.add(email);
    177             }
    178             Trace.endSection();
    179 
    180             Trace.beginSection("load contact photo bytes");
    181             // Query the contacts provider for the current batch of emails.
    182             final ImmutableMap<String, ContactInfo> contactInfos = loadContactPhotos(emails);
    183             Trace.endSection();
    184 
    185             for (ContactRequestHolder request : mContactRequests) {
    186                 Trace.beginSection("decode");
    187                 final String email = request.getEmail();
    188                 if (contactInfos == null) {
    189                     // Query failed.
    190                     LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
    191                     publishProgress(new Result(request, null));
    192                     Trace.endSection();
    193                     continue;
    194                 }
    195 
    196                 final ContactInfo contactInfo = contactInfos.get(email);
    197                 if (contactInfo == null) {
    198                     // Request skipped. Try again next batch.
    199                     LogUtils.d(TAG, "ContactResolver  = skipped %s", email);
    200                     Trace.endSection();
    201                     continue;
    202                 }
    203 
    204                 // Query attempted.
    205                 final byte[] photo = contactInfo.photoBytes;
    206                 if (photo == null) {
    207                     // No photo bytes found.
    208                     LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
    209                     publishProgress(new Result(request, null));
    210                     Trace.endSection();
    211                     continue;
    212                 }
    213 
    214                 // Query succeeded. Photo bytes found.
    215                 request.contactRequest.bytes = photo;
    216 
    217                 // Start decode.
    218                 LogUtils.d(TAG, "ContactResolver ++ found   %s", email);
    219                 // Synchronously decode the photo bytes. We are already in a background
    220                 // thread, and we want decodes to finish in order. The decodes are blazing
    221                 // fast so we don't need to kick off multiple threads.
    222                 final DecodeTask.DecodeOptions opts = new DecodeTask.DecodeOptions(
    223                         request.destination.getDecodeWidth(),
    224                         request.destination.getDecodeHeight(), 1 / 2f,
    225                         DecodeTask.DecodeOptions.STRATEGY_ROUND_NEAREST);
    226                 final ReusableBitmap result = new DecodeTask(request.contactRequest, opts, null,
    227                         null, mCache).decode();
    228                 request.contactRequest.bytes = null;
    229 
    230                 // Decode success.
    231                 publishProgress(new Result(request, result));
    232                 Trace.endSection();
    233             }
    234 
    235             return null;
    236         }
    237 
    238         protected ImmutableMap<String, ContactInfo> loadContactPhotos(Set<String> emails) {
    239             return SenderInfoLoader.loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
    240         }
    241 
    242         /**
    243          * We use progress updates to jump to the UI thread so we can decode the batch
    244          * incrementally.
    245          */
    246         @Override
    247         protected void onProgressUpdate(final Result... values) {
    248             final ContactRequestHolder request = values[0].request;
    249             final ReusableBitmap bitmap = values[0].bitmap;
    250 
    251             // DecodeTask does not add null results to the cache.
    252             if (bitmap == null) {
    253                 // Cache null result.
    254                 mCache.put(request.contactRequest, null);
    255             }
    256 
    257             request.destination.onDecodeComplete(request.contactRequest, bitmap);
    258         }
    259 
    260         @Override
    261         protected void onPostExecute(final Void aVoid) {
    262             // Batch completed. Start next batch.
    263             mCallback.notifyBatchReady();
    264         }
    265     }
    266 
    267     /**
    268      * Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
    269      * to onProgressUpdate().
    270      */
    271     private static class Result {
    272         public final ContactRequestHolder request;
    273         public final ReusableBitmap bitmap;
    274 
    275         private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
    276             this.request = request;
    277             this.bitmap = bitmap;
    278         }
    279     }
    280 }
    281