12a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/*
22a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Copyright (C) 2012 Google Inc.
32a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Licensed to The Android Open Source Project.
42a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) *
52a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Licensed under the Apache License, Version 2.0 (the "License");
62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * you may not use this file except in compliance with the License.
72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * You may obtain a copy of the License at
82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) *
92a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) *      http://www.apache.org/licenses/LICENSE-2.0
102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) *
112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * Unless required by applicable law or agreed to in writing, software
122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * distributed under the License is distributed on an "AS IS" BASIS,
13c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles) * See the License for the specific language governing permissions and
152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * limitations under the License.
162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */
17c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)
180529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochpackage com.android.mail;
195f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
20b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)import android.content.AsyncTaskLoader;
21c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)import android.content.ContentResolver;
222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.content.ContentUris;
2368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)import android.content.Context;
242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.content.Loader;
252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.database.Cursor;
262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.graphics.Bitmap;
272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.graphics.BitmapFactory;
28c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)import android.net.Uri;
292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.os.Build.VERSION;
302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.provider.ContactsContract.CommonDataKinds.Email;
312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.provider.ContactsContract.Contacts;
322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.provider.ContactsContract.Contacts.Photo;
331e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)import android.provider.ContactsContract.Data;
340529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochimport android.util.Pair;
3568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import com.android.bitmap.util.Trace;
372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import com.android.mail.utils.Utils;
382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import com.google.common.collect.ImmutableMap;
39c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)import com.google.common.collect.Maps;
40424c4d7b64af9d0d8fd9624f381f469654d5e3d2Torne (Richard Coles)
41cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)import java.util.ArrayList;
4268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)import java.util.Collection;
432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import java.util.Map;
442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import java.util.Set;
45b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)
465f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)/**
475f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles) * A {@link Loader} to look up presence, contact URI, and photo data for a set of email
4868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles) * addresses.
4968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles) */
50c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)public class SenderInfoLoader extends AsyncTaskLoader<ImmutableMap<String, ContactInfo>> {
5168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
5268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    private static final String[] DATA_COLS = new String[] {
5368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)        Email._ID,                  // 0
54c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        Email.DATA,                 // 1
555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        Email.CONTACT_ID,           // 2
562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        Email.PHOTO_ID,             // 3
572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    };
582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int DATA_EMAIL_COLUMN = 1;
592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int DATA_CONTACT_ID_COLUMN = 2;
602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int DATA_PHOTO_ID_COLUMN = 3;
612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
62b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)    private static final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
6368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    private static final int PHOTO_PHOTO_ID_COLUMN = 0;
642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int PHOTO_PHOTO_COLUMN = 1;
652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    /**
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
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 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     */
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
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
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
266    private static String[] toStringArray(Collection<String> items) {
267        return items.toArray(new String[items.size()]);
268    }
269
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