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
18package com.android.mail;
19
20import com.google.common.collect.ImmutableMap;
21import com.google.common.collect.Maps;
22
23import android.content.AsyncTaskLoader;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.Context;
27import android.content.Loader;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.net.Uri;
32import android.provider.ContactsContract.CommonDataKinds.Email;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Contacts.Photo;
35import android.provider.ContactsContract.Data;
36import android.util.Pair;
37
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.Map;
41import 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 */
48public 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