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 
SenderInfoLoader(Context context, Set<String> senders)74     public SenderInfoLoader(Context context, Set<String> senders) {
75         super(context);
76         mSenders = senders;
77     }
78 
79     @Override
onStartLoading()80     protected void onStartLoading() {
81         forceLoad();
82     }
83 
84     @Override
onStopLoading()85     protected void onStopLoading() {
86         cancelLoad();
87     }
88 
89     @Override
loadInBackground()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      */
loadContactPhotos( final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps)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 
contactInfoSortOrder()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 
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