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