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 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 
     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 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      */
    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 
    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 
    225     private static String[] toStringArray(Collection<String> items) {
    226         return items.toArray(new String[items.size()]);
    227     }
    228 
    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