Merge "Make photo loader low-memory device friendly" into ics-mr1
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 3cae01b..e742ba0 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -15,12 +15,6 @@
 -->
 
 <resources>
-    <!-- Amount of memory in bytes allocated for photo cache -->
-    <integer name="config_photo_cache_max_bytes">2000000</integer>
-
-    <!-- Number of decoded photo bitmaps retained in an LRU cache -->
-    <integer name="config_photo_cache_max_bitmaps">48</integer>
-
     <!-- Determines the number of columns in a ContactTileRow -->
     <integer name="contact_tile_column_count">2</integer>
 
diff --git a/src/com/android/contacts/ContactPhotoManager.java b/src/com/android/contacts/ContactPhotoManager.java
index 7b54666..7c54e80 100644
--- a/src/com/android/contacts/ContactPhotoManager.java
+++ b/src/com/android/contacts/ContactPhotoManager.java
@@ -17,14 +17,16 @@
 package com.android.contacts;
 
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.util.MemoryUtils;
 import com.android.contacts.util.UriUtils;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
 
+import android.content.ComponentCallbacks2;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.res.Resources;
+import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -40,6 +42,7 @@
 import android.provider.ContactsContract.Contacts.Photo;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.LruCache;
 import android.widget.ImageView;
@@ -52,13 +55,14 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Asynchronously loads contact photos and maintains a cache of photos.
  */
-public abstract class ContactPhotoManager {
-
+public abstract class ContactPhotoManager implements ComponentCallbacks2 {
     static final String TAG = "ContactPhotoManager";
+    static final boolean DEBUG = false; // Don't submit with true
 
     public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
 
@@ -177,6 +181,21 @@
      * preload photos.
      */
     public abstract void preloadPhotosInBackground();
+
+    // ComponentCallbacks2
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+    }
+
+    // ComponentCallbacks2
+    @Override
+    public void onLowMemory() {
+    }
+
+    // ComponentCallbacks2
+    @Override
+    public void onTrimMemory(int level) {
+    }
 }
 
 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
@@ -263,19 +282,115 @@
      */
     private boolean mPaused;
 
+    /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
+    private static final int HOLDER_CACHE_SIZE = 2000000;
+
+    /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
+    private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
+
+    private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
+
+    /** For debug: How many times we had to reload cached photo for a stale entry */
+    private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
+
+    /** For debug: How many times we had to reload cached photo for a fresh entry.  Should be 0. */
+    private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
+
     public ContactPhotoManagerImpl(Context context) {
         mContext = context;
 
-        Resources resources = context.getResources();
-        mBitmapCache = new LruCache<Object, Bitmap>(
-                resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
-        int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
-        mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
+        final float cacheSizeAdjustment =
+                (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
+        final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
+        mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
+            @Override protected int sizeOf(Object key, Bitmap value) {
+                return value.getByteCount();
+            }
+
+            @Override protected void entryRemoved(
+                    boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
+                if (DEBUG) dumpStats();
+            }
+        };
+        final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
+        mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
             @Override protected int sizeOf(Object key, BitmapHolder value) {
                 return value.bytes != null ? value.bytes.length : 0;
             }
+
+            @Override protected void entryRemoved(
+                    boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
+                if (DEBUG) dumpStats();
+            }
         };
-        mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
+        mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
+        Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
+        if (DEBUG) {
+            Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
+                    + " + " + btk(mBitmapCache.maxSize()));
+        }
+    }
+
+    /** Converts bytes to K bytes, rounding up.  Used only for debug log. */
+    private static String btk(int bytes) {
+        return ((bytes + 1023) / 1024) + "K";
+    }
+
+    private static final int safeDiv(int dividend, int divisor) {
+        return (divisor  == 0) ? 0 : (dividend / divisor);
+    }
+
+    /**
+     * Dump cache stats on logcat.
+     */
+    private void dumpStats() {
+        if (!DEBUG) return;
+        {
+            int numHolders = 0;
+            int rawBytes = 0;
+            int bitmapBytes = 0;
+            int numBitmaps = 0;
+            for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
+                numHolders++;
+                if (h.bytes != null) {
+                    rawBytes += h.bytes.length;
+                }
+                Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
+                if (b != null) {
+                    numBitmaps++;
+                    bitmapBytes += b.getByteCount();
+                }
+            }
+            Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
+                    + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
+                    + numBitmaps + " bitmaps, avg: "
+                    + btk(safeDiv(rawBytes, numHolders))
+                    + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
+            Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
+                    + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
+                    + " stale=" + mStaleCacheOverwrite.get());
+        }
+
+        {
+            int numBitmaps = 0;
+            int bitmapBytes = 0;
+            for (Bitmap b : mBitmapCache.snapshot().values()) {
+                numBitmaps++;
+                bitmapBytes += b.getByteCount();
+            }
+            Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
+                    + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
+            // We don't get from L2 cache, so L2 stats is meaningless.
+        }
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
+        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+            // Clear the caches.  Note all pending requests will be removed too.
+            clear();
+        }
     }
 
     @Override
@@ -292,6 +407,7 @@
             defaultProvider.applyDefaultImage(view, hires, darkTheme);
             mPendingRequests.remove(view);
         } else {
+            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
             loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
                     defaultProvider));
         }
@@ -305,6 +421,7 @@
             defaultProvider.applyDefaultImage(view, hires, darkTheme);
             mPendingRequests.remove(view);
         } else {
+            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
             loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
                     defaultProvider));
         }
@@ -331,6 +448,7 @@
 
     @Override
     public void refreshCache() {
+        if (DEBUG) Log.d(TAG, "refreshCache");
         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
             holder.fresh = false;
         }
@@ -375,7 +493,7 @@
      * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
      * the holder, it will not be necessary to decode the bitmap.
      */
-    private void inflateBitmap(BitmapHolder holder) {
+    private static void inflateBitmap(BitmapHolder holder) {
         byte[] bytes = holder.bytes;
         if (bytes == null || bytes.length == 0) {
             return;
@@ -394,14 +512,21 @@
             Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
             holder.bitmap = bitmap;
             holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
+            if (DEBUG) {
+                Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
+                        + bitmap.getWidth() + "x" + bitmap.getHeight()
+                        + ", " + btk(bitmap.getByteCount()));
+            }
         } catch (OutOfMemoryError e) {
             // Do nothing - the photo will appear to be missing
         }
     }
 
     public void clear() {
+        if (DEBUG) Log.d(TAG, "clear");
         mPendingRequests.clear();
         mBitmapHolderCache.evictAll();
+        mBitmapCache.evictAll();
     }
 
     @Override
@@ -412,6 +537,7 @@
     @Override
     public void resume() {
         mPaused = false;
+        if (DEBUG) dumpStats();
         if (!mPendingRequests.isEmpty()) {
             requestLoading();
         }
@@ -449,6 +575,7 @@
                 if (!mPaused) {
                     processLoadedImages();
                 }
+                if (DEBUG) dumpStats();
                 return true;
             }
         }
@@ -498,6 +625,18 @@
      * Stores the supplied bitmap in cache.
      */
     private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
+        if (DEBUG) {
+            BitmapHolder prev = mBitmapHolderCache.get(key);
+            if (prev != null && prev.bytes != null) {
+                Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
+                if (prev.fresh) {
+                    mFreshCacheOverwrite.incrementAndGet();
+                } else {
+                    mStaleCacheOverwrite.incrementAndGet();
+                }
+            }
+            Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
+        }
         BitmapHolder holder = new BitmapHolder(bytes);
         holder.fresh = true;
 
@@ -757,6 +896,7 @@
 
             Cursor cursor = null;
             try {
+                if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
                 cursor = mResolver.query(Data.CONTENT_URI,
                         COLUMNS,
                         mStringBuilder.toString(),
@@ -812,6 +952,7 @@
                     mBuffer = new byte[BUFFER_SIZE];
                 }
                 try {
+                    if (DEBUG) Log.d(TAG, "Loading " + uri);
                     InputStream is = mResolver.openInputStream(uri);
                     if (is != null) {
                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/src/com/android/contacts/ContactsApplication.java b/src/com/android/contacts/ContactsApplication.java
index 16f9e57..b23dde1 100644
--- a/src/com/android/contacts/ContactsApplication.java
+++ b/src/com/android/contacts/ContactsApplication.java
@@ -95,6 +95,7 @@
         if (ContactPhotoManager.CONTACT_PHOTO_SERVICE.equals(name)) {
             if (mContactPhotoManager == null) {
                 mContactPhotoManager = ContactPhotoManager.createContactPhotoManager(this);
+                registerComponentCallbacks(mContactPhotoManager);
                 mContactPhotoManager.preloadPhotosInBackground();
             }
             return mContactPhotoManager;
diff --git a/src/com/android/contacts/util/MemoryUtils.java b/src/com/android/contacts/util/MemoryUtils.java
new file mode 100644
index 0000000..f634151
--- /dev/null
+++ b/src/com/android/contacts/util/MemoryUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import com.android.internal.util.MemInfoReader;
+
+public final class MemoryUtils {
+    private MemoryUtils() {
+    }
+
+    private static long sTotalMemorySize = -1;
+
+    public static long getTotalMemorySize() {
+        if (sTotalMemorySize < 0) {
+            MemInfoReader reader = new MemInfoReader();
+            reader.readMemInfo();
+
+            // getTotalSize() returns the "MemTotal" value from /proc/meminfo.
+            // Because the linux kernel doesn't see all the RAM on the system (e.g. GPU takes some),
+            // this is usually smaller than the actual RAM size.
+            sTotalMemorySize = reader.getTotalSize();
+        }
+        return sTotalMemorySize;
+    }
+}