blob: b3d1fd536a3262c317c52dd3c6f3a864f3a85bf1 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.bitmap;
import android.content.ContentResolver;
import android.os.AsyncTask;
import android.os.AsyncTask.Status;
import android.os.Handler;
import com.android.bitmap.BitmapCache;
import com.android.bitmap.DecodeTask;
import com.android.bitmap.ReusableBitmap;
import com.android.ex.photo.util.Trace;
import com.android.mail.ContactInfo;
import com.android.mail.SenderInfoLoader;
import com.android.mail.bitmap.ContactRequest.ContactRequestHolder;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.google.common.collect.ImmutableMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Batches up ContactRequests so we can efficiently query the contacts provider. Kicks off a
* ContactResolverTask to query for contact images in the background.
*/
public class ContactResolver implements Runnable {
private static final String TAG = LogTag.getLogTag();
private final ContentResolver mResolver;
private final BitmapCache mCache;
/** Insertion ordered set allows us to work from the top down. */
private final LinkedHashSet<ContactRequestHolder> mBatch;
private final Handler mHandler = new Handler();
private ContactResolverTask mTask;
/** Size 1 pool mostly to make systrace output traces on one line. */
private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
mResolver = resolver;
mCache = cache;
mBatch = new LinkedHashSet<ContactRequestHolder>();
}
@Override
public void run() {
// Start to process a new batch.
if (mBatch.isEmpty()) {
return;
}
if (mTask != null && mTask.getStatus() == Status.RUNNING) {
LogUtils.d(TAG, "ContactResolver << batch skip");
return;
}
Trace.beginSection("ContactResolver run");
LogUtils.d(TAG, "ContactResolver >> batch start");
// Make a copy of the batch.
LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
if (mTask != null) {
mTask.cancel(true);
}
mTask = new ContactResolverTask(batch, mResolver, mCache, this);
mTask.executeOnExecutor(EXECUTOR);
Trace.endSection();
}
public void add(final ContactRequest request, final ContactDrawable drawable) {
mBatch.add(new ContactRequestHolder(request, drawable));
notifyBatchReady();
}
public void remove(final ContactRequest request, final ContactDrawable drawable) {
mBatch.remove(new ContactRequestHolder(request, drawable));
}
/**
* A layout pass traverses the whole tree during a single iteration of the event loop. That
* means that every ContactDrawable on the screen will add its ContactRequest to the batch in
* a single iteration of the event loop.
*
* <p/>
* We take advantage of this by posting a Runnable (happens to be this object) at the end of
* the event queue. Every time something is added to the batch as part of the same layout pass,
* the Runnable is moved to the back of the queue. When the next layout pass occurs,
* it is placed in the event loop behind this Runnable. That allows us to process the batch
* that was added previously.
*/
private void notifyBatchReady() {
LogUtils.d(TAG, "ContactResolver > batch %d", mBatch.size());
mHandler.removeCallbacks(this);
mHandler.post(this);
}
/**
* This is not a very traditional AsyncTask, in the sense that we do not care about what gets
* returned in doInBackground(). Instead, we signal traditional "return values" through
* publishProgress().
*
* <p/>
* The reason we do this is because this task is responsible for decoding an entire batch of
* ContactRequests. But, we do not want to have to wait to decode all of them before updating
* any views. So we must do all the work in doInBackground(),
* but upon finishing each individual task, we need to jump out to the UI thread and update
* that view.
*/
private static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
private final Set<ContactRequestHolder> mContactRequests;
private final ContentResolver mResolver;
private final BitmapCache mCache;
private final ContactResolver mCallback;
public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
final ContentResolver resolver, final BitmapCache cache,
final ContactResolver callback) {
mContactRequests = contactRequests;
mResolver = resolver;
mCache = cache;
mCallback = callback;
}
@Override
protected Void doInBackground(final Void... params) {
Trace.beginSection("set up");
final Set<String> emails = new HashSet<String>(mContactRequests.size());
for (ContactRequestHolder request : mContactRequests) {
final String email = request.getEmail();
emails.add(email);
}
Trace.endSection();
Trace.beginSection("load contact photo bytes");
// Query the contacts provider for the current batch of emails.
ImmutableMap<String, ContactInfo> contactInfos = SenderInfoLoader
.loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
Trace.endSection();
for (ContactRequestHolder request : mContactRequests) {
Trace.beginSection("decode");
final String email = request.getEmail();
if (contactInfos == null) {
// Query failed.
LogUtils.d(TAG, "ContactResolver -- failed %s", email);
publishProgress(new Result(request, null));
Trace.endSection();
continue;
}
final ContactInfo contactInfo = contactInfos.get(email);
if (contactInfo == null) {
// Request skipped. Try again next batch.
LogUtils.d(TAG, "ContactResolver = skipped %s", email);
Trace.endSection();
continue;
}
// Query attempted.
final byte[] photo = contactInfo.photoBytes;
if (photo == null) {
// No photo bytes found.
LogUtils.d(TAG, "ContactResolver -- failed %s", email);
publishProgress(new Result(request, null));
Trace.endSection();
continue;
}
// Query succeeded. Photo bytes found.
request.contactRequest.bytes = photo;
// Start decode.
LogUtils.d(TAG, "ContactResolver ++ found %s", email);
final ReusableBitmap result;
final int width = mCache.getDecodeWidth();
final int height = mCache.getDecodeHeight();
// Synchronously decode the photo bytes. We are already in a background
// thread, and we want decodes to finish in order. The decodes are blazing
// fast so we don't need to kick off multiple threads.
result = new DecodeTask(request.contactRequest, width, height, width, height, null,
mCache).decode();
request.contactRequest.bytes = null;
// Decode success.
publishProgress(new Result(request, result));
Trace.endSection();
}
return null;
}
/**
* We use progress updates to jump to the UI thread so we can decode the batch
* incrementally.
*/
@Override
protected void onProgressUpdate(final Result... values) {
final ContactRequestHolder request = values[0].request;
final ReusableBitmap bitmap = values[0].bitmap;
// DecodeTask does not add null results to the cache.
if (bitmap == null) {
// Cache null result.
mCache.put(request.contactRequest, null);
}
request.destination.onDecodeComplete(request.contactRequest, bitmap);
}
@Override
protected void onPostExecute(final Void aVoid) {
// Batch completed. Start next batch.
mCallback.notifyBatchReady();
}
}
/**
* Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
* to onProgressUpdate().
*/
private static class Result {
public final ContactRequestHolder request;
public final ReusableBitmap bitmap;
private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
this.request = request;
this.bitmap = bitmap;
}
}
}