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