• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onDecodeComplete(final RequestKey key, final ReusableBitmap result)68         public void onDecodeComplete(final RequestKey key, final ReusableBitmap result);
getDecodeWidth()69         public int getDecodeWidth();
getDecodeHeight()70         public int getDecodeHeight();
71     }
72 
ContactResolver(final ContentResolver resolver, final BitmapCache cache)73     public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
74         mResolver = resolver;
75         mCache = cache;
76         mBatch = new LinkedHashSet<ContactRequestHolder>();
77     }
78 
79     @Override
run()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 
getContactResolverTask( LinkedHashSet<ContactRequestHolder> batch)106     protected ContactResolverTask getContactResolverTask(
107             LinkedHashSet<ContactRequestHolder> batch) {
108         return new ContactResolverTask(batch, mResolver, mCache, this);
109     }
110 
getCache()111     public BitmapCache getCache() {
112         return mCache;
113     }
114 
add(final ContactRequest request, final ContactDrawableInterface drawable)115     public void add(final ContactRequest request, final ContactDrawableInterface drawable) {
116         mBatch.add(new ContactRequestHolder(request, drawable));
117         notifyBatchReady();
118     }
119 
remove(final ContactRequest request, final ContactDrawableInterface drawable)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      */
notifyBatchReady()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 
ContactResolverTask(final Set<ContactRequestHolder> contactRequests, final ContentResolver resolver, final BitmapCache cache, final ContactResolver callback)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
doInBackground(final Void... params)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 
loadContactPhotos(Set<String> emails)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
onProgressUpdate(final Result... values)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
onPostExecute(final Void aVoid)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 
Result(final ContactRequestHolder request, final ReusableBitmap bitmap)275         private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
276             this.request = request;
277             this.bitmap = bitmap;
278         }
279     }
280 }
281