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 com.google.common.collect.ImmutableMap; 21 import com.google.common.collect.Maps; 22 23 import android.content.AsyncTaskLoader; 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.Context; 27 import android.content.Loader; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.BitmapFactory; 31 import android.net.Uri; 32 import android.provider.ContactsContract.CommonDataKinds.Email; 33 import android.provider.ContactsContract.Contacts; 34 import android.provider.ContactsContract.Contacts.Photo; 35 import android.provider.ContactsContract.Data; 36 import android.util.Pair; 37 38 import java.util.ArrayList; 39 import java.util.Collection; 40 import java.util.Map; 41 import java.util.Set; 42 43 /** 44 * A {@link Loader} to look up presence, contact URI, and photo data for a set of email 45 * addresses. 46 * 47 */ 48 public class SenderInfoLoader extends AsyncTaskLoader<ImmutableMap<String, ContactInfo>> { 49 50 private static final String[] DATA_COLS = new String[] { 51 Email._ID, // 0 52 Email.DATA, // 1 53 Email.CONTACT_PRESENCE, // 2 54 Email.CONTACT_ID, // 3 55 Email.PHOTO_ID, // 4 56 }; 57 private static final int DATA_EMAIL_COLUMN = 1; 58 private static final int DATA_STATUS_COLUMN = 2; 59 private static final int DATA_CONTACT_ID_COLUMN = 3; 60 private static final int DATA_PHOTO_ID_COLUMN = 4; 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 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 senderSet 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 addresses to {@link ContactInfo}s. The {@link ContactInfo} will 107 * contain either a byte array or an actual decoded bitmap for the sender image. 108 */ 109 public static ImmutableMap<String, ContactInfo> loadContactPhotos( 110 final ContentResolver resolver, final Set<String> senderSet, 111 final boolean decodeBitmaps) { 112 Cursor cursor = null; 113 114 Map<String, ContactInfo> results = Maps.newHashMap(); 115 116 // temporary structures 117 Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap(); 118 ArrayList<String> photoIdsAsStrings = new ArrayList<String>(); 119 ArrayList<String> senders = getTruncatedQueryParams(senderSet); 120 121 // Build first query 122 StringBuilder query = new StringBuilder() 123 .append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE) 124 .append("' AND ").append(Email.DATA).append(" IN ("); 125 appendQuestionMarks(query, senders); 126 query.append(')'); 127 128 try { 129 cursor = resolver.query(Data.CONTENT_URI, DATA_COLS, 130 query.toString(), toStringArray(senders), null /* sortOrder */); 131 132 if (cursor == null) { 133 return null; 134 } 135 136 int i = -1; 137 while (cursor.moveToPosition(++i)) { 138 String email = cursor.getString(DATA_EMAIL_COLUMN); 139 long contactId = cursor.getLong(DATA_CONTACT_ID_COLUMN); 140 Integer status = null; 141 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 142 143 if (!cursor.isNull(DATA_STATUS_COLUMN)) { 144 status = cursor.getInt(DATA_STATUS_COLUMN); 145 } 146 147 ContactInfo result = new ContactInfo(contactUri, status); 148 149 if (!cursor.isNull(DATA_PHOTO_ID_COLUMN)) { 150 long photoId = cursor.getLong(DATA_PHOTO_ID_COLUMN); 151 photoIdsAsStrings.add(Long.toString(photoId)); 152 photoIdMap.put(photoId, Pair.create(email, result)); 153 } 154 results.put(email, result); 155 } 156 cursor.close(); 157 158 if (photoIdsAsStrings.isEmpty()) { 159 return ImmutableMap.copyOf(results); 160 } 161 162 // Build second query: photoIDs->blobs 163 // based on photo batch-select code in ContactPhotoManager 164 photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings); 165 query.setLength(0); 166 query.append(Photo._ID).append(" IN ("); 167 appendQuestionMarks(query, photoIdsAsStrings); 168 query.append(')'); 169 170 cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS, 171 query.toString(), toStringArray(photoIdsAsStrings), null /* sortOrder */); 172 173 if (cursor == null) { 174 return ImmutableMap.copyOf(results); 175 } 176 177 i = -1; 178 while (cursor.moveToPosition(++i)) { 179 byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN); 180 if (photoBytes == null) { 181 continue; 182 } 183 184 long photoId = cursor.getLong(PHOTO_PHOTO_ID_COLUMN); 185 Pair<String, ContactInfo> prev = photoIdMap.get(photoId); 186 String email = prev.first; 187 ContactInfo prevResult = prev.second; 188 189 if (decodeBitmaps) { 190 Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); 191 // overwrite existing photo-less result 192 results.put(email, 193 new ContactInfo(prevResult.contactUri, prevResult.status, photo)); 194 } else { 195 // overwrite existing photoBytes-less result 196 results.put(email, new ContactInfo( 197 prevResult.contactUri, prevResult.status, photoBytes)); 198 } 199 } 200 } finally { 201 if (cursor != null) { 202 cursor.close(); 203 } 204 } 205 206 return ImmutableMap.copyOf(results); 207 } 208 209 static ArrayList<String> getTruncatedQueryParams(Collection<String> params) { 210 int truncatedLen = Math.min(params.size(), MAX_QUERY_PARAMS); 211 ArrayList<String> truncated = new ArrayList<String>(truncatedLen); 212 213 int copied = 0; 214 for (String param : params) { 215 truncated.add(param); 216 copied++; 217 if (copied >= truncatedLen) { 218 break; 219 } 220 } 221 222 return truncated; 223 } 224 225 private static String[] toStringArray(Collection<String> items) { 226 return items.toArray(new String[items.size()]); 227 } 228 229 static void appendQuestionMarks(StringBuilder query, Iterable<?> items) { 230 boolean first = true; 231 for (Object item : items) { 232 if (first) { 233 first = false; 234 } else { 235 query.append(','); 236 } 237 query.append('?'); 238 } 239 } 240 241 } 242