1/*
2 * Copyright (C) 2010 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.contacts;
18
19import android.content.ComponentCallbacks2;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Paint;
30import android.graphics.Paint.Style;
31import android.graphics.drawable.BitmapDrawable;
32import android.graphics.drawable.ColorDrawable;
33import android.graphics.drawable.Drawable;
34import android.graphics.drawable.TransitionDrawable;
35import android.net.Uri;
36import android.os.Handler;
37import android.os.Handler.Callback;
38import android.os.HandlerThread;
39import android.os.Message;
40import android.provider.ContactsContract;
41import android.provider.ContactsContract.Contacts;
42import android.provider.ContactsContract.Contacts.Photo;
43import android.provider.ContactsContract.Data;
44import android.provider.ContactsContract.Directory;
45import android.text.TextUtils;
46import android.util.Log;
47import android.util.LruCache;
48import android.util.TypedValue;
49import android.widget.ImageView;
50
51import com.android.contacts.model.AccountTypeManager;
52import com.android.contacts.util.BitmapUtil;
53import com.android.contacts.util.MemoryUtils;
54import com.android.contacts.util.UriUtils;
55import com.google.common.collect.Lists;
56import com.google.common.collect.Sets;
57
58import java.io.ByteArrayOutputStream;
59import java.io.InputStream;
60import java.lang.ref.Reference;
61import java.lang.ref.SoftReference;
62import java.util.Iterator;
63import java.util.List;
64import java.util.Set;
65import java.util.concurrent.ConcurrentHashMap;
66import java.util.concurrent.atomic.AtomicInteger;
67
68/**
69 * Asynchronously loads contact photos and maintains a cache of photos.
70 */
71public abstract class ContactPhotoManager implements ComponentCallbacks2 {
72    static final String TAG = "ContactPhotoManager";
73    static final boolean DEBUG = false; // Don't submit with true
74    static final boolean DEBUG_SIZES = false; // Don't submit with true
75
76    /** Caches 180dip in pixel. This is used to detect whether to show the hires or lores version
77     * of the default avatar */
78    private static int s180DipInPixel = -1;
79
80    public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
81
82    /**
83     * Returns the resource id of the default avatar. Tries to find a resource that is bigger
84     * than the given extent (width or height). If extent=-1, a thumbnail avatar is returned
85     */
86    public static int getDefaultAvatarResId(Context context, int extent, boolean darkTheme) {
87        // TODO: Is it worth finding a nicer way to do hires/lores here? In practice, the
88        // default avatar doesn't look too different when stretched
89        if (s180DipInPixel == -1) {
90            Resources r = context.getResources();
91            s180DipInPixel = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 180,
92                    r.getDisplayMetrics());
93        }
94
95        final boolean hires = (extent != -1) && (extent > s180DipInPixel);
96        return getDefaultAvatarResId(hires, darkTheme);
97    }
98
99    public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
100        if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
101        if (hires) return R.drawable.ic_contact_picture_180_holo_light;
102        if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
103        return R.drawable.ic_contact_picture_holo_light;
104    }
105
106    public static abstract class DefaultImageProvider {
107        /**
108         * Applies the default avatar to the ImageView. Extent is an indicator for the size (width
109         * or height). If darkTheme is set, the avatar is one that looks better on dark background
110         */
111        public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme);
112    }
113
114    private static class AvatarDefaultImageProvider extends DefaultImageProvider {
115        @Override
116        public void applyDefaultImage(ImageView view, int extent, boolean darkTheme) {
117            view.setImageResource(getDefaultAvatarResId(view.getContext(), extent, darkTheme));
118        }
119    }
120
121    private static class BlankDefaultImageProvider extends DefaultImageProvider {
122        private static Drawable sDrawable;
123
124        @Override
125        public void applyDefaultImage(ImageView view, int extent, boolean darkTheme) {
126            if (sDrawable == null) {
127                Context context = view.getContext();
128                sDrawable = new ColorDrawable(context.getResources().getColor(
129                        R.color.image_placeholder));
130            }
131            view.setImageDrawable(sDrawable);
132        }
133    }
134
135    public static final DefaultImageProvider DEFAULT_AVATAR = new AvatarDefaultImageProvider();
136
137    public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
138
139    /**
140     * Requests the singleton instance of {@link AccountTypeManager} with data bound from
141     * the available authenticators. This method can safely be called from the UI thread.
142     */
143    public static ContactPhotoManager getInstance(Context context) {
144        Context applicationContext = context.getApplicationContext();
145        ContactPhotoManager service =
146                (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
147        if (service == null) {
148            service = createContactPhotoManager(applicationContext);
149            Log.e(TAG, "No contact photo service in context: " + applicationContext);
150        }
151        return service;
152    }
153
154    public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
155        return new ContactPhotoManagerImpl(context);
156    }
157
158    /**
159     * Load thumbnail image into the supplied image view. If the photo is already cached,
160     * it is displayed immediately.  Otherwise a request is sent to load the photo
161     * from the database.
162     */
163    public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
164            DefaultImageProvider defaultProvider);
165
166    /**
167     * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageProvider)} with
168     * {@link #DEFAULT_AVATAR}.
169     */
170    public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme) {
171        loadThumbnail(view, photoId, darkTheme, DEFAULT_AVATAR);
172    }
173
174    /**
175     * Load photo into the supplied image view. If the photo is already cached,
176     * it is displayed immediately. Otherwise a request is sent to load the photo
177     * from the location specified by the URI.
178     * @param view The target view
179     * @param photoUri The uri of the photo to load
180     * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
181     * This is useful if the source image can be a lot bigger that the target, so that the decoding
182     * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
183     * is performed
184     * @param darkTheme Whether the background is dark. This is used for default avatars
185     * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
186     * refer to an existing image)
187     */
188    public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
189            boolean darkTheme, DefaultImageProvider defaultProvider);
190
191    /**
192     * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
193     * {@link #DEFAULT_AVATAR}.
194     */
195    public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
196            boolean darkTheme) {
197        loadPhoto(view, photoUri, requestedExtent, darkTheme, DEFAULT_AVATAR);
198    }
199
200    /**
201     * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
202     * {@link #DEFAULT_AVATAR} and with the assumption, that the image is a thumbnail
203     */
204    public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme) {
205        loadPhoto(view, photoUri, -1, darkTheme, DEFAULT_AVATAR);
206    }
207
208    /**
209     * Remove photo from the supplied image view. This also cancels current pending load request
210     * inside this photo manager.
211     */
212    public abstract void removePhoto(ImageView view);
213
214    /**
215     * Temporarily stops loading photos from the database.
216     */
217    public abstract void pause();
218
219    /**
220     * Resumes loading photos from the database.
221     */
222    public abstract void resume();
223
224    /**
225     * Marks all cached photos for reloading.  We can continue using cache but should
226     * also make sure the photos haven't changed in the background and notify the views
227     * if so.
228     */
229    public abstract void refreshCache();
230
231    /**
232     * Stores the given bitmap directly in the LRU bitmap cache.
233     * @param photoUri The URI of the photo (for future requests).
234     * @param bitmap The bitmap.
235     * @param photoBytes The bytes that were parsed to create the bitmap.
236     */
237    public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
238
239    /**
240     * Initiates a background process that over time will fill up cache with
241     * preload photos.
242     */
243    public abstract void preloadPhotosInBackground();
244
245    // ComponentCallbacks2
246    @Override
247    public void onConfigurationChanged(Configuration newConfig) {
248    }
249
250    // ComponentCallbacks2
251    @Override
252    public void onLowMemory() {
253    }
254
255    // ComponentCallbacks2
256    @Override
257    public void onTrimMemory(int level) {
258    }
259}
260
261class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
262    private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
263
264    private static final int FADE_TRANSITION_DURATION = 200;
265
266    /**
267     * Type of message sent by the UI thread to itself to indicate that some photos
268     * need to be loaded.
269     */
270    private static final int MESSAGE_REQUEST_LOADING = 1;
271
272    /**
273     * Type of message sent by the loader thread to indicate that some photos have
274     * been loaded.
275     */
276    private static final int MESSAGE_PHOTOS_LOADED = 2;
277
278    private static final String[] EMPTY_STRING_ARRAY = new String[0];
279
280    private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
281
282    /**
283     * Maintains the state of a particular photo.
284     */
285    private static class BitmapHolder {
286        final byte[] bytes;
287        final int originalSmallerExtent;
288
289        volatile boolean fresh;
290        Bitmap bitmap;
291        Reference<Bitmap> bitmapRef;
292        int decodedSampleSize;
293
294        public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
295            this.bytes = bytes;
296            this.fresh = true;
297            this.originalSmallerExtent = originalSmallerExtent;
298        }
299    }
300
301    private final Context mContext;
302
303    /**
304     * An LRU cache for bitmap holders. The cache contains bytes for photos just
305     * as they come from the database. Each holder has a soft reference to the
306     * actual bitmap.
307     */
308    private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
309
310    /**
311     * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
312     */
313    private volatile boolean mBitmapHolderCacheAllUnfresh = true;
314
315    /**
316     * Cache size threshold at which bitmaps will not be preloaded.
317     */
318    private final int mBitmapHolderCacheRedZoneBytes;
319
320    /**
321     * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
322     * the most recently used bitmaps to save time on decoding
323     * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
324     */
325    private final LruCache<Object, Bitmap> mBitmapCache;
326
327    /**
328     * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
329     * The request may swapped out before the photo loading request is started.
330     */
331    private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
332            new ConcurrentHashMap<ImageView, Request>();
333
334    /**
335     * Handler for messages sent to the UI thread.
336     */
337    private final Handler mMainThreadHandler = new Handler(this);
338
339    /**
340     * Thread responsible for loading photos from the database. Created upon
341     * the first request.
342     */
343    private LoaderThread mLoaderThread;
344
345    /**
346     * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
347     */
348    private boolean mLoadingRequested;
349
350    /**
351     * Flag indicating if the image loading is paused.
352     */
353    private boolean mPaused;
354
355    /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
356    private static final int HOLDER_CACHE_SIZE = 2000000;
357
358    /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
359    private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
360
361    private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
362
363    /** For debug: How many times we had to reload cached photo for a stale entry */
364    private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
365
366    /** For debug: How many times we had to reload cached photo for a fresh entry.  Should be 0. */
367    private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
368
369    public ContactPhotoManagerImpl(Context context) {
370        mContext = context;
371
372        final float cacheSizeAdjustment =
373                (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
374        final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
375        mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
376            @Override protected int sizeOf(Object key, Bitmap value) {
377                return value.getByteCount();
378            }
379
380            @Override protected void entryRemoved(
381                    boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
382                if (DEBUG) dumpStats();
383            }
384        };
385        final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
386        mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
387            @Override protected int sizeOf(Object key, BitmapHolder value) {
388                return value.bytes != null ? value.bytes.length : 0;
389            }
390
391            @Override protected void entryRemoved(
392                    boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
393                if (DEBUG) dumpStats();
394            }
395        };
396        mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
397        Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
398        if (DEBUG) {
399            Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
400                    + " + " + btk(mBitmapCache.maxSize()));
401        }
402    }
403
404    /** Converts bytes to K bytes, rounding up.  Used only for debug log. */
405    private static String btk(int bytes) {
406        return ((bytes + 1023) / 1024) + "K";
407    }
408
409    private static final int safeDiv(int dividend, int divisor) {
410        return (divisor  == 0) ? 0 : (dividend / divisor);
411    }
412
413    /**
414     * Dump cache stats on logcat.
415     */
416    private void dumpStats() {
417        if (!DEBUG) return;
418        {
419            int numHolders = 0;
420            int rawBytes = 0;
421            int bitmapBytes = 0;
422            int numBitmaps = 0;
423            for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
424                numHolders++;
425                if (h.bytes != null) {
426                    rawBytes += h.bytes.length;
427                }
428                Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
429                if (b != null) {
430                    numBitmaps++;
431                    bitmapBytes += b.getByteCount();
432                }
433            }
434            Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
435                    + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
436                    + numBitmaps + " bitmaps, avg: "
437                    + btk(safeDiv(rawBytes, numHolders))
438                    + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
439            Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
440                    + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
441                    + " stale=" + mStaleCacheOverwrite.get());
442        }
443
444        {
445            int numBitmaps = 0;
446            int bitmapBytes = 0;
447            for (Bitmap b : mBitmapCache.snapshot().values()) {
448                numBitmaps++;
449                bitmapBytes += b.getByteCount();
450            }
451            Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
452                    + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
453            // We don't get from L2 cache, so L2 stats is meaningless.
454        }
455    }
456
457    @Override
458    public void onTrimMemory(int level) {
459        if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
460        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
461            // Clear the caches.  Note all pending requests will be removed too.
462            clear();
463        }
464    }
465
466    @Override
467    public void preloadPhotosInBackground() {
468        ensureLoaderThread();
469        mLoaderThread.requestPreloading();
470    }
471
472    @Override
473    public void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
474            DefaultImageProvider defaultProvider) {
475        if (photoId == 0) {
476            // No photo is needed
477            defaultProvider.applyDefaultImage(view, -1, darkTheme);
478            mPendingRequests.remove(view);
479        } else {
480            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
481            loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme,
482                    defaultProvider));
483        }
484    }
485
486    @Override
487    public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
488            DefaultImageProvider defaultProvider) {
489        if (photoUri == null) {
490            // No photo is needed
491            defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme);
492            mPendingRequests.remove(view);
493        } else {
494            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
495            loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent, darkTheme,
496                    defaultProvider));
497        }
498    }
499
500    private void loadPhotoByIdOrUri(ImageView view, Request request) {
501        boolean loaded = loadCachedPhoto(view, request, false);
502        if (loaded) {
503            mPendingRequests.remove(view);
504        } else {
505            mPendingRequests.put(view, request);
506            if (!mPaused) {
507                // Send a request to start loading photos
508                requestLoading();
509            }
510        }
511    }
512
513    @Override
514    public void removePhoto(ImageView view) {
515        view.setImageDrawable(null);
516        mPendingRequests.remove(view);
517    }
518
519    @Override
520    public void refreshCache() {
521        if (mBitmapHolderCacheAllUnfresh) {
522            if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
523            return;
524        }
525        if (DEBUG) Log.d(TAG, "refreshCache");
526        mBitmapHolderCacheAllUnfresh = true;
527        for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
528            holder.fresh = false;
529        }
530    }
531
532    /**
533     * Checks if the photo is present in cache.  If so, sets the photo on the view.
534     *
535     * @return false if the photo needs to be (re)loaded from the provider.
536     */
537    private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
538        BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
539        if (holder == null) {
540            // The bitmap has not been loaded ==> show default avatar
541            request.applyDefaultImage(view);
542            return false;
543        }
544
545        if (holder.bytes == null) {
546            request.applyDefaultImage(view);
547            return holder.fresh;
548        }
549
550        Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
551        if (cachedBitmap == null) {
552            if (holder.bytes.length < 8 * 1024) {
553                // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
554                inflateBitmap(holder, request.getRequestedExtent());
555                cachedBitmap = holder.bitmap;
556                if (cachedBitmap == null) return false;
557            } else {
558                // This is bigger data. Let's send that back to the Loader so that we can
559                // inflate this in the background
560                request.applyDefaultImage(view);
561                return false;
562            }
563        }
564
565        final Drawable previousDrawable = view.getDrawable();
566        if (fadeIn && previousDrawable != null) {
567            final Drawable[] layers = new Drawable[2];
568            // Prevent cascade of TransitionDrawables.
569            if (previousDrawable instanceof TransitionDrawable) {
570                final TransitionDrawable previousTransitionDrawable =
571                        (TransitionDrawable) previousDrawable;
572                layers[0] = previousTransitionDrawable.getDrawable(
573                        previousTransitionDrawable.getNumberOfLayers() - 1);
574            } else {
575                layers[0] = previousDrawable;
576            }
577            layers[1] = new BitmapDrawable(mContext.getResources(), cachedBitmap);
578            TransitionDrawable drawable = new TransitionDrawable(layers);
579            view.setImageDrawable(drawable);
580            drawable.startTransition(FADE_TRANSITION_DURATION);
581        } else {
582            view.setImageBitmap(cachedBitmap);
583        }
584
585        // Put the bitmap in the LRU cache. But only do this for images that are small enough
586        // (we require that at least six of those can be cached at the same time)
587        if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
588            mBitmapCache.put(request.getKey(), cachedBitmap);
589        }
590
591        // Soften the reference
592        holder.bitmap = null;
593
594        return holder.fresh;
595    }
596
597    /**
598     * If necessary, decodes bytes stored in the holder to Bitmap.  As long as the
599     * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
600     * the holder, it will not be necessary to decode the bitmap.
601     */
602    private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
603        final int sampleSize =
604                BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
605        byte[] bytes = holder.bytes;
606        if (bytes == null || bytes.length == 0) {
607            return;
608        }
609
610        if (sampleSize == holder.decodedSampleSize) {
611            // Check the soft reference.  If will be retained if the bitmap is also
612            // in the LRU cache, so we don't need to check the LRU cache explicitly.
613            if (holder.bitmapRef != null) {
614                holder.bitmap = holder.bitmapRef.get();
615                if (holder.bitmap != null) {
616                    return;
617                }
618            }
619        }
620
621        try {
622            Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
623
624            // make bitmap mutable and draw size onto it
625            if (DEBUG_SIZES) {
626                Bitmap original = bitmap;
627                bitmap = bitmap.copy(bitmap.getConfig(), true);
628                original.recycle();
629                Canvas canvas = new Canvas(bitmap);
630                Paint paint = new Paint();
631                paint.setTextSize(16);
632                paint.setColor(Color.BLUE);
633                paint.setStyle(Style.FILL);
634                canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
635                paint.setColor(Color.WHITE);
636                paint.setAntiAlias(true);
637                canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
638            }
639
640            holder.decodedSampleSize = sampleSize;
641            holder.bitmap = bitmap;
642            holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
643            if (DEBUG) {
644                Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
645                        + bitmap.getWidth() + "x" + bitmap.getHeight()
646                        + ", " + btk(bitmap.getByteCount()));
647            }
648        } catch (OutOfMemoryError e) {
649            // Do nothing - the photo will appear to be missing
650        }
651    }
652
653    public void clear() {
654        if (DEBUG) Log.d(TAG, "clear");
655        mPendingRequests.clear();
656        mBitmapHolderCache.evictAll();
657        mBitmapCache.evictAll();
658    }
659
660    @Override
661    public void pause() {
662        mPaused = true;
663    }
664
665    @Override
666    public void resume() {
667        mPaused = false;
668        if (DEBUG) dumpStats();
669        if (!mPendingRequests.isEmpty()) {
670            requestLoading();
671        }
672    }
673
674    /**
675     * Sends a message to this thread itself to start loading images.  If the current
676     * view contains multiple image views, all of those image views will get a chance
677     * to request their respective photos before any of those requests are executed.
678     * This allows us to load images in bulk.
679     */
680    private void requestLoading() {
681        if (!mLoadingRequested) {
682            mLoadingRequested = true;
683            mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
684        }
685    }
686
687    /**
688     * Processes requests on the main thread.
689     */
690    @Override
691    public boolean handleMessage(Message msg) {
692        switch (msg.what) {
693            case MESSAGE_REQUEST_LOADING: {
694                mLoadingRequested = false;
695                if (!mPaused) {
696                    ensureLoaderThread();
697                    mLoaderThread.requestLoading();
698                }
699                return true;
700            }
701
702            case MESSAGE_PHOTOS_LOADED: {
703                if (!mPaused) {
704                    processLoadedImages();
705                }
706                if (DEBUG) dumpStats();
707                return true;
708            }
709        }
710        return false;
711    }
712
713    public void ensureLoaderThread() {
714        if (mLoaderThread == null) {
715            mLoaderThread = new LoaderThread(mContext.getContentResolver());
716            mLoaderThread.start();
717        }
718    }
719
720    /**
721     * Goes over pending loading requests and displays loaded photos.  If some of the
722     * photos still haven't been loaded, sends another request for image loading.
723     */
724    private void processLoadedImages() {
725        Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
726        while (iterator.hasNext()) {
727            ImageView view = iterator.next();
728            Request key = mPendingRequests.get(view);
729            boolean loaded = loadCachedPhoto(view, key, true);
730            if (loaded) {
731                iterator.remove();
732            }
733        }
734
735        softenCache();
736
737        if (!mPendingRequests.isEmpty()) {
738            requestLoading();
739        }
740    }
741
742    /**
743     * Removes strong references to loaded bitmaps to allow them to be garbage collected
744     * if needed.  Some of the bitmaps will still be retained by {@link #mBitmapCache}.
745     */
746    private void softenCache() {
747        for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
748            holder.bitmap = null;
749        }
750    }
751
752    /**
753     * Stores the supplied bitmap in cache.
754     */
755    private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
756        if (DEBUG) {
757            BitmapHolder prev = mBitmapHolderCache.get(key);
758            if (prev != null && prev.bytes != null) {
759                Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
760                if (prev.fresh) {
761                    mFreshCacheOverwrite.incrementAndGet();
762                } else {
763                    mStaleCacheOverwrite.incrementAndGet();
764                }
765            }
766            Log.d(TAG, "Caching data: key=" + key + ", " +
767                    (bytes == null ? "<null>" : btk(bytes.length)));
768        }
769        BitmapHolder holder = new BitmapHolder(bytes,
770                bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
771
772        // Unless this image is being preloaded, decode it right away while
773        // we are still on the background thread.
774        if (!preloading) {
775            inflateBitmap(holder, requestedExtent);
776        }
777
778        mBitmapHolderCache.put(key, holder);
779        mBitmapHolderCacheAllUnfresh = false;
780    }
781
782    @Override
783    public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
784        final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
785        // We can pretend here that the extent of the photo was the size that we originally
786        // requested
787        Request request = Request.createFromUri(photoUri, smallerExtent, false, DEFAULT_AVATAR);
788        BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
789        holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
790        mBitmapHolderCache.put(request.getKey(), holder);
791        mBitmapHolderCacheAllUnfresh = false;
792        mBitmapCache.put(request.getKey(), bitmap);
793    }
794
795    /**
796     * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
797     * already loaded
798     */
799    private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
800            Set<String> photoIdsAsStrings, Set<Request> uris) {
801        photoIds.clear();
802        photoIdsAsStrings.clear();
803        uris.clear();
804
805        boolean jpegsDecoded = false;
806
807        /*
808         * Since the call is made from the loader thread, the map could be
809         * changing during the iteration. That's not really a problem:
810         * ConcurrentHashMap will allow those changes to happen without throwing
811         * exceptions. Since we may miss some requests in the situation of
812         * concurrent change, we will need to check the map again once loading
813         * is complete.
814         */
815        Iterator<Request> iterator = mPendingRequests.values().iterator();
816        while (iterator.hasNext()) {
817            Request request = iterator.next();
818            final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
819            if (holder != null && holder.bytes != null && holder.fresh &&
820                    (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
821                // This was previously loaded but we don't currently have the inflated Bitmap
822                inflateBitmap(holder, request.getRequestedExtent());
823                jpegsDecoded = true;
824            } else {
825                if (holder == null || !holder.fresh) {
826                    if (request.isUriRequest()) {
827                        uris.add(request);
828                    } else {
829                        photoIds.add(request.getId());
830                        photoIdsAsStrings.add(String.valueOf(request.mId));
831                    }
832                }
833            }
834        }
835
836        if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
837    }
838
839    /**
840     * The thread that performs loading of photos from the database.
841     */
842    private class LoaderThread extends HandlerThread implements Callback {
843        private static final int BUFFER_SIZE = 1024*16;
844        private static final int MESSAGE_PRELOAD_PHOTOS = 0;
845        private static final int MESSAGE_LOAD_PHOTOS = 1;
846
847        /**
848         * A pause between preload batches that yields to the UI thread.
849         */
850        private static final int PHOTO_PRELOAD_DELAY = 1000;
851
852        /**
853         * Number of photos to preload per batch.
854         */
855        private static final int PRELOAD_BATCH = 25;
856
857        /**
858         * Maximum number of photos to preload.  If the cache size is 2Mb and
859         * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
860         */
861        private static final int MAX_PHOTOS_TO_PRELOAD = 100;
862
863        private final ContentResolver mResolver;
864        private final StringBuilder mStringBuilder = new StringBuilder();
865        private final Set<Long> mPhotoIds = Sets.newHashSet();
866        private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
867        private final Set<Request> mPhotoUris = Sets.newHashSet();
868        private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
869
870        private Handler mLoaderThreadHandler;
871        private byte mBuffer[];
872
873        private static final int PRELOAD_STATUS_NOT_STARTED = 0;
874        private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
875        private static final int PRELOAD_STATUS_DONE = 2;
876
877        private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
878
879        public LoaderThread(ContentResolver resolver) {
880            super(LOADER_THREAD_NAME);
881            mResolver = resolver;
882        }
883
884        public void ensureHandler() {
885            if (mLoaderThreadHandler == null) {
886                mLoaderThreadHandler = new Handler(getLooper(), this);
887            }
888        }
889
890        /**
891         * Kicks off preloading of the next batch of photos on the background thread.
892         * Preloading will happen after a delay: we want to yield to the UI thread
893         * as much as possible.
894         * <p>
895         * If preloading is already complete, does nothing.
896         */
897        public void requestPreloading() {
898            if (mPreloadStatus == PRELOAD_STATUS_DONE) {
899                return;
900            }
901
902            ensureHandler();
903            if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
904                return;
905            }
906
907            mLoaderThreadHandler.sendEmptyMessageDelayed(
908                    MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
909        }
910
911        /**
912         * Sends a message to this thread to load requested photos.  Cancels a preloading
913         * request, if any: we don't want preloading to impede loading of the photos
914         * we need to display now.
915         */
916        public void requestLoading() {
917            ensureHandler();
918            mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
919            mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
920        }
921
922        /**
923         * Receives the above message, loads photos and then sends a message
924         * to the main thread to process them.
925         */
926        @Override
927        public boolean handleMessage(Message msg) {
928            switch (msg.what) {
929                case MESSAGE_PRELOAD_PHOTOS:
930                    preloadPhotosInBackground();
931                    break;
932                case MESSAGE_LOAD_PHOTOS:
933                    loadPhotosInBackground();
934                    break;
935            }
936            return true;
937        }
938
939        /**
940         * The first time it is called, figures out which photos need to be preloaded.
941         * Each subsequent call preloads the next batch of photos and requests
942         * another cycle of preloading after a delay.  The whole process ends when
943         * we either run out of photos to preload or fill up cache.
944         */
945        private void preloadPhotosInBackground() {
946            if (mPreloadStatus == PRELOAD_STATUS_DONE) {
947                return;
948            }
949
950            if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
951                queryPhotosForPreload();
952                if (mPreloadPhotoIds.isEmpty()) {
953                    mPreloadStatus = PRELOAD_STATUS_DONE;
954                } else {
955                    mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
956                }
957                requestPreloading();
958                return;
959            }
960
961            if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
962                mPreloadStatus = PRELOAD_STATUS_DONE;
963                return;
964            }
965
966            mPhotoIds.clear();
967            mPhotoIdsAsStrings.clear();
968
969            int count = 0;
970            int preloadSize = mPreloadPhotoIds.size();
971            while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
972                preloadSize--;
973                count++;
974                Long photoId = mPreloadPhotoIds.get(preloadSize);
975                mPhotoIds.add(photoId);
976                mPhotoIdsAsStrings.add(photoId.toString());
977                mPreloadPhotoIds.remove(preloadSize);
978            }
979
980            loadThumbnails(true);
981
982            if (preloadSize == 0) {
983                mPreloadStatus = PRELOAD_STATUS_DONE;
984            }
985
986            Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
987                    + mBitmapHolderCache.size());
988
989            requestPreloading();
990        }
991
992        private void queryPhotosForPreload() {
993            Cursor cursor = null;
994            try {
995                Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
996                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
997                        .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
998                                String.valueOf(MAX_PHOTOS_TO_PRELOAD))
999                        .build();
1000                cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
1001                        Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1002                        null,
1003                        Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1004
1005                if (cursor != null) {
1006                    while (cursor.moveToNext()) {
1007                        // Insert them in reverse order, because we will be taking
1008                        // them from the end of the list for loading.
1009                        mPreloadPhotoIds.add(0, cursor.getLong(0));
1010                    }
1011                }
1012            } finally {
1013                if (cursor != null) {
1014                    cursor.close();
1015                }
1016            }
1017        }
1018
1019        private void loadPhotosInBackground() {
1020            obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1021            loadThumbnails(false);
1022            loadUriBasedPhotos();
1023            requestPreloading();
1024        }
1025
1026        /** Loads thumbnail photos with ids */
1027        private void loadThumbnails(boolean preloading) {
1028            if (mPhotoIds.isEmpty()) {
1029                return;
1030            }
1031
1032            // Remove loaded photos from the preload queue: we don't want
1033            // the preloading process to load them again.
1034            if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1035                for (Long id : mPhotoIds) {
1036                    mPreloadPhotoIds.remove(id);
1037                }
1038                if (mPreloadPhotoIds.isEmpty()) {
1039                    mPreloadStatus = PRELOAD_STATUS_DONE;
1040                }
1041            }
1042
1043            mStringBuilder.setLength(0);
1044            mStringBuilder.append(Photo._ID + " IN(");
1045            for (int i = 0; i < mPhotoIds.size(); i++) {
1046                if (i != 0) {
1047                    mStringBuilder.append(',');
1048                }
1049                mStringBuilder.append('?');
1050            }
1051            mStringBuilder.append(')');
1052
1053            Cursor cursor = null;
1054            try {
1055                if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1056                cursor = mResolver.query(Data.CONTENT_URI,
1057                        COLUMNS,
1058                        mStringBuilder.toString(),
1059                        mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1060                        null);
1061
1062                if (cursor != null) {
1063                    while (cursor.moveToNext()) {
1064                        Long id = cursor.getLong(0);
1065                        byte[] bytes = cursor.getBlob(1);
1066                        cacheBitmap(id, bytes, preloading, -1);
1067                        mPhotoIds.remove(id);
1068                    }
1069                }
1070            } finally {
1071                if (cursor != null) {
1072                    cursor.close();
1073                }
1074            }
1075
1076            // Remaining photos were not found in the contacts database (but might be in profile).
1077            for (Long id : mPhotoIds) {
1078                if (ContactsContract.isProfileId(id)) {
1079                    Cursor profileCursor = null;
1080                    try {
1081                        profileCursor = mResolver.query(
1082                                ContentUris.withAppendedId(Data.CONTENT_URI, id),
1083                                COLUMNS, null, null, null);
1084                        if (profileCursor != null && profileCursor.moveToFirst()) {
1085                            cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
1086                                    preloading, -1);
1087                        } else {
1088                            // Couldn't load a photo this way either.
1089                            cacheBitmap(id, null, preloading, -1);
1090                        }
1091                    } finally {
1092                        if (profileCursor != null) {
1093                            profileCursor.close();
1094                        }
1095                    }
1096                } else {
1097                    // Not a profile photo and not found - mark the cache accordingly
1098                    cacheBitmap(id, null, preloading, -1);
1099                }
1100            }
1101
1102            mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1103        }
1104
1105        /**
1106         * Loads photos referenced with Uris. Those can be remote thumbnails
1107         * (from directory searches), display photos etc
1108         */
1109        private void loadUriBasedPhotos() {
1110            for (Request uriRequest : mPhotoUris) {
1111                Uri uri = uriRequest.getUri();
1112                if (mBuffer == null) {
1113                    mBuffer = new byte[BUFFER_SIZE];
1114                }
1115                try {
1116                    if (DEBUG) Log.d(TAG, "Loading " + uri);
1117                    InputStream is = mResolver.openInputStream(uri);
1118                    if (is != null) {
1119                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
1120                        try {
1121                            int size;
1122                            while ((size = is.read(mBuffer)) != -1) {
1123                                baos.write(mBuffer, 0, size);
1124                            }
1125                        } finally {
1126                            is.close();
1127                        }
1128                        cacheBitmap(uri, baos.toByteArray(), false,
1129                                uriRequest.getRequestedExtent());
1130                        mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1131                    } else {
1132                        Log.v(TAG, "Cannot load photo " + uri);
1133                        cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
1134                    }
1135                } catch (Exception ex) {
1136                    Log.v(TAG, "Cannot load photo " + uri, ex);
1137                    cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
1138                }
1139            }
1140        }
1141    }
1142
1143    /**
1144     * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1145     * light theme
1146     */
1147    private static final class Request {
1148        private final long mId;
1149        private final Uri mUri;
1150        private final boolean mDarkTheme;
1151        private final int mRequestedExtent;
1152        private final DefaultImageProvider mDefaultProvider;
1153
1154        private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
1155                DefaultImageProvider defaultProvider) {
1156            mId = id;
1157            mUri = uri;
1158            mDarkTheme = darkTheme;
1159            mRequestedExtent = requestedExtent;
1160            mDefaultProvider = defaultProvider;
1161        }
1162
1163        public static Request createFromThumbnailId(long id, boolean darkTheme,
1164                DefaultImageProvider defaultProvider) {
1165            return new Request(id, null /* no URI */, -1, darkTheme, defaultProvider);
1166        }
1167
1168        public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1169                DefaultImageProvider defaultProvider) {
1170            return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, defaultProvider);
1171        }
1172
1173        public boolean isUriRequest() {
1174            return mUri != null;
1175        }
1176
1177        public Uri getUri() {
1178            return mUri;
1179        }
1180
1181        public long getId() {
1182            return mId;
1183        }
1184
1185        public int getRequestedExtent() {
1186            return mRequestedExtent;
1187        }
1188
1189        @Override
1190        public int hashCode() {
1191            final int prime = 31;
1192            int result = 1;
1193            result = prime * result + (int) (mId ^ (mId >>> 32));
1194            result = prime * result + mRequestedExtent;
1195            result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
1196            return result;
1197        }
1198
1199        @Override
1200        public boolean equals(Object obj) {
1201            if (this == obj) return true;
1202            if (obj == null) return false;
1203            if (getClass() != obj.getClass()) return false;
1204            final Request that = (Request) obj;
1205            if (mId != that.mId) return false;
1206            if (mRequestedExtent != that.mRequestedExtent) return false;
1207            if (!UriUtils.areEqual(mUri, that.mUri)) return false;
1208            // Don't compare equality of mDarkTheme because it is only used in the default contact
1209            // photo case. When the contact does have a photo, the contact photo is the same
1210            // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
1211            // twice.
1212            return true;
1213        }
1214
1215        public Object getKey() {
1216            return mUri == null ? mId : mUri;
1217        }
1218
1219        public void applyDefaultImage(ImageView view) {
1220            mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme);
1221        }
1222    }
1223}
1224