• 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 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 
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 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      */
loadContactPhotos( final ContentResolver resolver, final Set<String> senderSet, final boolean decodeBitmaps)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 
getTruncatedQueryParams(Collection<String> params)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 
toStringArray(Collection<String> items)225     private static String[] toStringArray(Collection<String> items) {
226         return items.toArray(new String[items.size()]);
227     }
228 
appendQuestionMarks(StringBuilder query, Iterable<?> items)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