• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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