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 {
onDecodeComplete(final RequestKey key, final ReusableBitmap result)72         public void onDecodeComplete(final RequestKey key, final ReusableBitmap result);
getDecodeWidth()73         public int getDecodeWidth();
getDecodeHeight()74         public int getDecodeHeight();
75     }
76 
ContactResolver(final ContentResolver resolver, final BitmapCache cache)77     public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
78         mResolver = resolver;
79         mCache = cache;
80         mBatch = new LinkedHashSet<ContactRequestHolder>();
81     }
82 
83     @Override
run()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 
getContactResolverTask( LinkedHashSet<ContactRequestHolder> batch)110     protected ContactResolverTask getContactResolverTask(
111             LinkedHashSet<ContactRequestHolder> batch) {
112         return new ContactResolverTask(batch, mResolver, mCache, this);
113     }
114 
getCache()115     public BitmapCache getCache() {
116         return mCache;
117     }
118 
add(final ContactRequest request, final ContactDrawableInterface drawable)119     public void add(final ContactRequest request, final ContactDrawableInterface drawable) {
120         mBatch.add(new ContactRequestHolder(request, drawable));
121         notifyBatchReady();
122     }
123 
remove(final ContactRequest request, final ContactDrawableInterface drawable)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      */
notifyBatchReady()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 
ContactResolverTask(final Set<ContactRequestHolder> contactRequests, final ContentResolver resolver, final BitmapCache cache, final ContactResolver callback)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
doInBackground(final Void... params)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 
loadContactPhotos(Set<String> emails)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
onProgressUpdate(final Result... values)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
onPostExecute(final Void aVoid)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 
Result(final ContactRequestHolder request, final ReusableBitmap bitmap)284         private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
285             this.request = request;
286             this.bitmap = bitmap;
287         }
288     }
289 }
290