Home | History | Annotate | Download | only in mail
      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 
     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