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