blob: c69e9341fef732aed9ac2f11e80907f73c4c7ed4 [file] [log] [blame]
/*
* Copyright (C) 2012 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;
import com.android.bitmap.Trace;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Photo;
import android.provider.ContactsContract.Data;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* A {@link Loader} to look up presence, contact URI, and photo data for a set of email
* addresses.
*
*/
public class SenderInfoLoader extends AsyncTaskLoader<ImmutableMap<String, ContactInfo>> {
private static final String[] DATA_COLS = new String[] {
Email._ID, // 0
Email.DATA, // 1
Email.CONTACT_PRESENCE, // 2
Email.CONTACT_ID, // 3
Email.PHOTO_ID, // 4
};
private static final int DATA_EMAIL_COLUMN = 1;
private static final int DATA_STATUS_COLUMN = 2;
private static final int DATA_CONTACT_ID_COLUMN = 3;
private static final int DATA_PHOTO_ID_COLUMN = 4;
private static final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
private static final int PHOTO_PHOTO_ID_COLUMN = 0;
private static final int PHOTO_PHOTO_COLUMN = 1;
/**
* Limit the query params to avoid hitting the maximum of 99. We choose a number smaller than
* 99 since the contacts provider may wrap our query in its own and insert more params.
*/
static final int MAX_QUERY_PARAMS = 75;
private final Set<String> mSenders;
public SenderInfoLoader(Context context, Set<String> senders) {
super(context);
mSenders = senders;
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public ImmutableMap<String, ContactInfo> loadInBackground() {
if (mSenders == null || mSenders.isEmpty()) {
return null;
}
return loadContactPhotos(
getContext().getContentResolver(), mSenders, true /* decodeBitmaps */);
}
/**
* Loads contact photos from the ContentProvider.
* @param resolver {@link ContentResolver} to use in queries to the ContentProvider.
* @param emails The email addresses of the sender images to return.
* @param decodeBitmaps If {@code true}, decode the bitmaps and put them into
* {@link ContactInfo}. Otherwise, just put the raw bytes of the photo
* into the {@link ContactInfo}.
* @return A mapping of email to {@link ContactInfo}. How to interpret the map:
* <ul>
* <li>The email is missing from the key set or maps to null - The email was skipped. Try
* again.</li>
* <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null -
* Photo loaded successfully.</li>
* <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null -
* Photo load failed.</li>
* </ul>
*/
public static ImmutableMap<String, ContactInfo> loadContactPhotos(
final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) {
Trace.beginSection("load contact photos util");
Cursor cursor = null;
Trace.beginSection("build first query");
Map<String, ContactInfo> results = Maps.newHashMap();
// temporary structures
Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap();
ArrayList<String> photoIdsAsStrings = new ArrayList<String>();
ArrayList<String> emailsList = getTruncatedQueryParams(emails);
// Build first query
StringBuilder query = new StringBuilder()
.append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE)
.append("' AND ").append(Email.DATA).append(" IN (");
appendQuestionMarks(query, emailsList);
query.append(')');
Trace.endSection();
try {
Trace.beginSection("query 1");
cursor = resolver.query(Data.CONTENT_URI, DATA_COLS,
query.toString(), toStringArray(emailsList), null /* sortOrder */);
Trace.endSection();
if (cursor == null) {
Trace.endSection();
return null;
}
Trace.beginSection("get photo id");
int i = -1;
while (cursor.moveToPosition(++i)) {
String email = cursor.getString(DATA_EMAIL_COLUMN);
long contactId = cursor.getLong(DATA_CONTACT_ID_COLUMN);
Integer status = null;
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
if (!cursor.isNull(DATA_STATUS_COLUMN)) {
status = cursor.getInt(DATA_STATUS_COLUMN);
}
ContactInfo result = new ContactInfo(contactUri, status);
if (!cursor.isNull(DATA_PHOTO_ID_COLUMN)) {
long photoId = cursor.getLong(DATA_PHOTO_ID_COLUMN);
photoIdsAsStrings.add(Long.toString(photoId));
photoIdMap.put(photoId, Pair.create(email, result));
}
results.put(email, result);
}
cursor.close();
Trace.endSection();
// Put empty ContactInfo for all the emails that didn't map to a contact.
// This allows us to differentiate between lookup failed,
// and lookup skipped (truncated above).
for (String email : emailsList) {
if (!results.containsKey(email)) {
results.put(email, new ContactInfo(null, null));
}
}
if (photoIdsAsStrings.isEmpty()) {
Trace.endSection();
return ImmutableMap.copyOf(results);
}
Trace.beginSection("build second query");
// Build second query: photoIDs->blobs
// based on photo batch-select code in ContactPhotoManager
photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings);
query.setLength(0);
query.append(Photo._ID).append(" IN (");
appendQuestionMarks(query, photoIdsAsStrings);
query.append(')');
Trace.endSection();
Trace.beginSection("query 2");
cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS,
query.toString(), toStringArray(photoIdsAsStrings), null /* sortOrder */);
Trace.endSection();
if (cursor == null) {
Trace.endSection();
return ImmutableMap.copyOf(results);
}
Trace.beginSection("get photo blob");
i = -1;
while (cursor.moveToPosition(++i)) {
byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN);
if (photoBytes == null) {
continue;
}
long photoId = cursor.getLong(PHOTO_PHOTO_ID_COLUMN);
Pair<String, ContactInfo> prev = photoIdMap.get(photoId);
String email = prev.first;
ContactInfo prevResult = prev.second;
if (decodeBitmaps) {
Trace.beginSection("decode bitmap");
Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
Trace.endSection();
// overwrite existing photo-less result
results.put(email,
new ContactInfo(prevResult.contactUri, prevResult.status, photo));
} else {
// overwrite existing photoBytes-less result
results.put(email, new ContactInfo(
prevResult.contactUri, prevResult.status, photoBytes));
}
}
Trace.endSection();
} finally {
if (cursor != null) {
cursor.close();
}
}
Trace.endSection();
return ImmutableMap.copyOf(results);
}
static ArrayList<String> getTruncatedQueryParams(Collection<String> params) {
int truncatedLen = Math.min(params.size(), MAX_QUERY_PARAMS);
ArrayList<String> truncated = new ArrayList<String>(truncatedLen);
int copied = 0;
for (String param : params) {
truncated.add(param);
copied++;
if (copied >= truncatedLen) {
break;
}
}
return truncated;
}
private static String[] toStringArray(Collection<String> items) {
return items.toArray(new String[items.size()]);
}
static void appendQuestionMarks(StringBuilder query, Iterable<?> items) {
boolean first = true;
for (Object item : items) {
if (first) {
first = false;
} else {
query.append(',');
}
query.append('?');
}
}
}