DefaultPhotoManager.java revision 35e82d4f9522906f7953667cf5c5f8137ec2f5ac
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.chips;
18
19import android.content.ContentResolver;
20import android.database.Cursor;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.net.Uri;
24import android.os.AsyncTask;
25import android.provider.ContactsContract;
26import android.support.v4.util.LruCache;
27import android.util.Log;
28
29import java.io.ByteArrayOutputStream;
30import java.io.FileNotFoundException;
31import java.io.IOException;
32import java.io.InputStream;
33
34/**
35 * Default implementation of {@link com.android.ex.chips.PhotoManager} that
36 * queries for photo bytes by using the {@link com.android.ex.chips.RecipientEntry}'s
37 * photoThumbnailUri.
38 */
39public class DefaultPhotoManager implements PhotoManager {
40    private static final String TAG = "DefaultPhotoManager";
41
42    private static final boolean DEBUG = false;
43
44    /**
45     * For reading photos for directory contacts, this is the chunk size for
46     * copying from the {@link InputStream} to the output stream.
47     */
48    private static final int BUFFER_SIZE = 1024*16;
49
50    private static class PhotoQuery {
51        public static final String[] PROJECTION = {
52            ContactsContract.CommonDataKinds.Photo.PHOTO
53        };
54
55        public static final int PHOTO = 0;
56    }
57
58    private final ContentResolver mContentResolver;
59    private final LruCache<Uri, byte[]> mPhotoCacheMap;
60
61    public DefaultPhotoManager(ContentResolver contentResolver) {
62        mContentResolver = contentResolver;
63        mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE);
64    }
65
66    @Override
67    public void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback) {
68        final Uri photoThumbnailUri = entry.getPhotoThumbnailUri();
69        if (photoThumbnailUri != null) {
70            final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri);
71            if (photoBytes != null) {
72                entry.setPhotoBytes(photoBytes);
73                // notifyDataSetChanged() should be called by a caller.
74            } else {
75                if (DEBUG) {
76                    Log.d(TAG, "No photo cache for " + entry.getDisplayName()
77                            + ". Fetch one asynchronously");
78                }
79                fetchPhotoAsync(entry, photoThumbnailUri, callback);
80            }
81        }
82    }
83
84    private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri,
85            final PhotoManagerCallback callback) {
86        final AsyncTask<Void, Void, byte[]> photoLoadTask = new AsyncTask<Void, Void, byte[]>() {
87            @Override
88            protected byte[] doInBackground(Void... params) {
89                // First try running a query. Images for local contacts are
90                // loaded by sending a query to the ContactsProvider.
91                final Cursor photoCursor = mContentResolver.query(
92                        photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null);
93                if (photoCursor != null) {
94                    try {
95                        if (photoCursor.moveToFirst()) {
96                            return photoCursor.getBlob(PhotoQuery.PHOTO);
97                        }
98                    } finally {
99                        photoCursor.close();
100                    }
101                } else {
102                    // If the query fails, try streaming the URI directly.
103                    // For remote directory images, this URI resolves to the
104                    // directory provider and the images are loaded by sending
105                    // an openFile call to the provider.
106                    try {
107                        InputStream is = mContentResolver.openInputStream(
108                                photoThumbnailUri);
109                        if (is != null) {
110                            byte[] buffer = new byte[BUFFER_SIZE];
111                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
112                            try {
113                                int size;
114                                while ((size = is.read(buffer)) != -1) {
115                                    baos.write(buffer, 0, size);
116                                }
117                            } finally {
118                                is.close();
119                            }
120                            return baos.toByteArray();
121                        }
122                    } catch (IOException ex) {
123                        // ignore
124                    }
125                }
126                return null;
127            }
128
129            @Override
130            protected void onPostExecute(final byte[] photoBytes) {
131                entry.setPhotoBytes(photoBytes);
132                if (photoBytes != null) {
133                    mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
134                    if (callback != null) {
135                        callback.onPhotoBytesAsynchronouslyPopulated();
136                    }
137                }
138            }
139        };
140        photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
141    }
142}
143