1 /* 2 * Copyright (C) 2012 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; 19 20 import android.content.AsyncTaskLoader; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.Context; 24 import android.content.Loader; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.net.Uri; 29 import android.os.Build.VERSION; 30 import android.provider.ContactsContract.CommonDataKinds.Email; 31 import android.provider.ContactsContract.Contacts; 32 import android.provider.ContactsContract.Contacts.Photo; 33 import android.provider.ContactsContract.Data; 34 import android.util.Pair; 35 36 import com.android.bitmap.util.Trace; 37 import com.android.mail.utils.Utils; 38 import com.google.common.collect.ImmutableMap; 39 import com.google.common.collect.Maps; 40 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.Map; 44 import java.util.Set; 45 46 /** 47 * A {@link Loader} to look up presence, contact URI, and photo data for a set of email 48 * addresses. 49 */ 50 public class SenderInfoLoader extends AsyncTaskLoader<ImmutableMap<String, ContactInfo>> { 51 52 private static final String[] DATA_COLS = new String[] { 53 Email._ID, // 0 54 Email.DATA, // 1 55 Email.CONTACT_ID, // 2 56 Email.PHOTO_ID, // 3 57 }; 58 private static final int DATA_EMAIL_COLUMN = 1; 59 private static final int DATA_CONTACT_ID_COLUMN = 2; 60 private static final int DATA_PHOTO_ID_COLUMN = 3; 61 62 private static final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO }; 63 private static final int PHOTO_PHOTO_ID_COLUMN = 0; 64 private static final int PHOTO_PHOTO_COLUMN = 1; 65 66 /** 67 * Limit the query params to avoid hitting the maximum of 99. We choose a number smaller than 68 * 99 since the contacts provider may wrap our query in its own and insert more params. 69 */ 70 private static final int MAX_QUERY_PARAMS = 75; 71 72 private final Set<String> mSenders; 73 74 public SenderInfoLoader(Context context, Set<String> senders) { 75 super(context); 76 mSenders = senders; 77 } 78 79 @Override 80 protected void onStartLoading() { 81 forceLoad(); 82 } 83 84 @Override 85 protected void onStopLoading() { 86 cancelLoad(); 87 } 88 89 @Override 90 public ImmutableMap<String, ContactInfo> loadInBackground() { 91 if (mSenders == null || mSenders.isEmpty()) { 92 return null; 93 } 94 95 return loadContactPhotos( 96 getContext().getContentResolver(), mSenders, true /* decodeBitmaps */); 97 } 98 99 /** 100 * Loads contact photos from the ContentProvider. 101 * @param resolver {@link ContentResolver} to use in queries to the ContentProvider. 102 * @param emails The email addresses of the sender images to return. 103 * @param decodeBitmaps If {@code true}, decode the bitmaps and put them into 104 * {@link ContactInfo}. Otherwise, just put the raw bytes of the photo 105 * into the {@link ContactInfo}. 106 * @return A mapping of email to {@link ContactInfo}. How to interpret the map: 107 * <ul> 108 * <li>The email is missing from the key set or maps to null - The email was skipped. Try 109 * again.</li> 110 * <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null - 111 * Photo loaded successfully.</li> 112 * <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null - 113 * Photo load failed.</li> 114 * </ul> 115 */ 116 public static ImmutableMap<String, ContactInfo> loadContactPhotos( 117 final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) { 118 Trace.beginSection("load contact photos util"); 119 Cursor cursor = null; 120 121 Trace.beginSection("build first query"); 122 Map<String, ContactInfo> results = Maps.newHashMap(); 123 124 // temporary structures 125 Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap(); 126 ArrayList<String> photoIdsAsStrings = new ArrayList<String>(); 127 ArrayList<String> emailsList = getTruncatedQueryParams(emails); 128 129 // Build first query 130 StringBuilder query = new StringBuilder() 131 .append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE) 132 .append("' AND ").append(Email.DATA).append(" IN ("); 133 appendQuestionMarks(query, emailsList); 134 query.append(')'); 135 Trace.endSection(); 136 137 // Contacts that are designed to be visible outside of search will be returned last. 138 // Therefore, these contacts will be given precedence below, if possible. 139 final String sortOrder = contactInfoSortOrder(); 140 141 try { 142 Trace.beginSection("query 1"); 143 cursor = resolver.query(Data.CONTENT_URI, DATA_COLS, 144 query.toString(), toStringArray(emailsList), sortOrder); 145 Trace.endSection(); 146 147 if (cursor == null) { 148 Trace.endSection(); 149 return null; 150 } 151 152 Trace.beginSection("get photo id"); 153 int i = -1; 154 while (cursor.moveToPosition(++i)) { 155 String email = cursor.getString(DATA_EMAIL_COLUMN); 156 long contactId = cursor.getLong(DATA_CONTACT_ID_COLUMN); 157 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 158 159 ContactInfo result = new ContactInfo(contactUri); 160 161 if (!cursor.isNull(DATA_PHOTO_ID_COLUMN)) { 162 long photoId = cursor.getLong(DATA_PHOTO_ID_COLUMN); 163 photoIdsAsStrings.add(Long.toString(photoId)); 164 photoIdMap.put(photoId, Pair.create(email, result)); 165 } 166 results.put(email, result); 167 } 168 cursor.close(); 169 Trace.endSection(); 170 171 // Put empty ContactInfo for all the emails that didn't map to a contact. 172 // This allows us to differentiate between lookup failed, 173 // and lookup skipped (truncated above). 174 for (String email : emailsList) { 175 if (!results.containsKey(email)) { 176 results.put(email, new ContactInfo(null)); 177 } 178 } 179 180 if (photoIdsAsStrings.isEmpty()) { 181 Trace.endSection(); 182 return ImmutableMap.copyOf(results); 183 } 184 185 Trace.beginSection("build second query"); 186 // Build second query: photoIDs->blobs 187 // based on photo batch-select code in ContactPhotoManager 188 photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings); 189 query.setLength(0); 190 query.append(Photo._ID).append(" IN ("); 191 appendQuestionMarks(query, photoIdsAsStrings); 192 query.append(')'); 193 Trace.endSection(); 194 195 Trace.beginSection("query 2"); 196 cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS, 197 query.toString(), toStringArray(photoIdsAsStrings), sortOrder); 198 Trace.endSection(); 199 200 if (cursor == null) { 201 Trace.endSection(); 202 return ImmutableMap.copyOf(results); 203 } 204 205 Trace.beginSection("get photo blob"); 206 i = -1; 207 while (cursor.moveToPosition(++i)) { 208 byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN); 209 if (photoBytes == null) { 210 continue; 211 } 212 213 long photoId = cursor.getLong(PHOTO_PHOTO_ID_COLUMN); 214 Pair<String, ContactInfo> prev = photoIdMap.get(photoId); 215 String email = prev.first; 216 ContactInfo prevResult = prev.second; 217 218 if (decodeBitmaps) { 219 Trace.beginSection("decode bitmap"); 220 Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); 221 Trace.endSection(); 222 // overwrite existing photo-less result 223 results.put(email, new ContactInfo(prevResult.contactUri, photo)); 224 } else { 225 // overwrite existing photoBytes-less result 226 results.put(email, new ContactInfo(prevResult.contactUri, photoBytes)); 227 } 228 } 229 Trace.endSection(); 230 } finally { 231 if (cursor != null) { 232 cursor.close(); 233 } 234 } 235 236 Trace.endSection(); 237 return ImmutableMap.copyOf(results); 238 } 239 240 private static String contactInfoSortOrder() { 241 // The ContactsContract.IN_DEFAULT_DIRECTORY does not exist prior to android L. There is 242 // no VERSION.SDK_INT value assigned for android L yet. Therefore, we must gate the 243 // following logic on the development codename. 244 if (Utils.isRunningLOrLater()) { 245 return Contacts.IN_DEFAULT_DIRECTORY + " ASC, " + Data._ID; 246 } 247 return null; 248 } 249 250 private static ArrayList<String> getTruncatedQueryParams(Collection<String> params) { 251 int truncatedLen = Math.min(params.size(), MAX_QUERY_PARAMS); 252 ArrayList<String> truncated = new ArrayList<String>(truncatedLen); 253 254 int copied = 0; 255 for (String param : params) { 256 truncated.add(param); 257 copied++; 258 if (copied >= truncatedLen) { 259 break; 260 } 261 } 262 263 return truncated; 264 } 265 266 private static String[] toStringArray(Collection<String> items) { 267 return items.toArray(new String[items.size()]); 268 } 269 270 private static void appendQuestionMarks(StringBuilder query, Iterable<?> items) { 271 boolean first = true; 272 for (Object item : items) { 273 if (first) { 274 first = false; 275 } else { 276 query.append(','); 277 } 278 query.append('?'); 279 } 280 } 281 282 } 283