am 3d99bb36: Disable RTL support
* commit '3d99bb36823013aba84de6917a4a64c6a264f5e9':
Disable RTL support
diff --git a/proguard.flags b/proguard.flags
index e0b2c23..7bab921 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -60,7 +60,6 @@
-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
*** setAnimatedHeightFraction(...);
- *** setPhotoFlipFraction(...);
}
-keepclasseswithmembers class com.android.mail.ui.MailActivity {
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 2693db7..0cfb9b2 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -93,10 +93,10 @@
<item>#ad62a7</item>
</array>
<color name="letter_tile_default_color">#d66161</color>
-
<color name="letter_tile_font_color">#ffffff</color>
-
<color name="tile_divider_color">#ffffff</color>
+ <!-- Color.GRAY -->
+ <color name="checkmark_tile_background_color">#ff888888</color>
<!-- Teaser colors -->
diff --git a/res/values/constants.xml b/res/values/constants.xml
index c238bf7..2f6cfc0 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -119,5 +119,5 @@
<integer name="ap_overflow_max_count">99</integer>
<!-- Duration of the animations for entering/exiting CAB mode -->
- <integer name="conv_item_view_cab_anim_duration">350</integer>
+ <integer name="conv_item_view_cab_anim_duration">250</integer>
</resources>
diff --git a/src/com/android/bitmap/AltBitmapCache.java b/src/com/android/bitmap/AltBitmapCache.java
index fb8e915..f519c7b 100644
--- a/src/com/android/bitmap/AltBitmapCache.java
+++ b/src/com/android/bitmap/AltBitmapCache.java
@@ -16,25 +16,40 @@
package com.android.bitmap;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap.NullReusableBitmap;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.LruCache;
/**
* This subclass provides custom pool behavior. The pool can be set to block on {@link #poll()} if
* nothing can be returned. This is useful if you know you will incur high costs upon receiving
* nothing from the pool, and you do not want to incur those costs at the critical moment when the
* UI is animating.
+ *
+ * This subclass provides custom cache behavior. Null values can be cached. Later,
+ * when the same key is used to retrieve the value, a {@link NullReusableBitmap} singleton will
+ * be returned.
*/
-public class AltBitmapCache extends AltPooledCache<DecodeTask.Request, ReusableBitmap>
+public class AltBitmapCache extends AltPooledCache<Request, ReusableBitmap>
implements BitmapCache {
private boolean mBlocking = false;
private final Object mLock = new Object();
+ private final LruCache<Request, Void> mNullRequests;
+
private final static boolean DEBUG = false;
private final static String TAG = LogTag.getLogTag();
- public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction) {
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+
+ public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction,
+ final int nullCapacity) {
super(targetSizeBytes, nonPooledFraction);
+
+ mNullRequests = new LruCache<Request, Void>(nullCapacity);
}
/**
@@ -78,7 +93,7 @@
LogUtils.d(TAG, "AltBitmapCache: %s notified",
Thread.currentThread().getName());
}
- } catch (InterruptedException e) {
+ } catch (InterruptedException ignored) {
}
Trace.endSection();
}
@@ -95,4 +110,50 @@
mLock.notify();
}
}
+
+ @Override
+ public ReusableBitmap get(final Request key, final boolean incrementRefCount) {
+ if (mNullRequests.containsKey(key)) {
+ return NullReusableBitmap.getInstance();
+ }
+ return super.get(key, incrementRefCount);
+ }
+
+ @Override
+ public ReusableBitmap put(final Request key, final ReusableBitmap value) {
+ if (value == null || value == NullReusableBitmap.getInstance()) {
+ mNullRequests.put(key, null);
+ return null;
+ }
+
+ // Do not allow the pool to be filled with bitmaps that are of the wrong dimensions.
+ if (mDecodeWidth > value.bmp.getWidth() || mDecodeHeight > value.bmp.getHeight()) {
+ if (DEBUG) {
+ LogUtils.d(TAG, "Discarding ReusableBitmap size %d x %d for cache size %d x %d.",
+ value.bmp.getWidth(), value.bmp.getHeight(), mDecodeWidth, mDecodeHeight);
+ }
+ return null;
+ }
+
+ return super.put(key, value);
+ }
+
+ @Override
+ public void setPoolDimensions(final int decodeWidth, final int decodeHeight) {
+ if (mDecodeWidth < decodeWidth || mDecodeHeight < decodeHeight) {
+ clear();
+ mDecodeWidth = decodeWidth;
+ mDecodeHeight = decodeHeight;
+ }
+ }
+
+ @Override
+ public int getDecodeWidth() {
+ return mDecodeWidth;
+ }
+
+ @Override
+ public int getDecodeHeight() {
+ return mDecodeHeight;
+ }
}
diff --git a/src/com/android/bitmap/AltPooledCache.java b/src/com/android/bitmap/AltPooledCache.java
index 0a7d7b4..03a6c50 100644
--- a/src/com/android/bitmap/AltPooledCache.java
+++ b/src/com/android/bitmap/AltPooledCache.java
@@ -67,6 +67,7 @@
@Override
public V get(K key, boolean incrementRefCount) {
+ Trace.beginSection("cache get");
synchronized (mCache) {
V result = mCache.get(key);
if (result == null && mNonPooledCache != null) {
@@ -75,12 +76,14 @@
if (incrementRefCount && result != null) {
result.acquireReference();
}
+ Trace.endSection();
return result;
}
}
@Override
public V put(K key, V value) {
+ Trace.beginSection("cache put");
synchronized (mCache) {
final V prev;
if (value.isEligibleForPooling()) {
@@ -90,22 +93,27 @@
} else {
prev = null;
}
+ Trace.endSection();
return prev;
}
}
@Override
public void offer(V value) {
+ Trace.beginSection("pool offer");
if (value.getRefCount() != 0 || !value.isEligibleForPooling()) {
throw new IllegalArgumentException("unexpected offer of an invalid object: " + value);
}
mPool.offer(value);
+ Trace.endSection();
}
@Override
public V poll() {
+ Trace.beginSection("pool poll");
final V pooled = mPool.poll();
if (pooled != null) {
+ Trace.endSection();
return pooled;
}
@@ -131,11 +139,13 @@
if (DEBUG) System.err.println(
"POOL SCAVENGE FAILED, cache not fully warm yet. szDelta="
+ (mTargetSize-unrefSize));
+ Trace.endSection();
return null;
} else {
mCache.remove(eldestUnref.getKey());
if (DEBUG) System.err.println("POOL SCAVENGE SUCCESS, oldKey="
+ eldestUnref.getKey());
+ Trace.endSection();
return eldestUnref.getValue();
}
}
@@ -209,4 +219,9 @@
}
+ @Override
+ public void clear() {
+ mCache.clear();
+ mPool.clear();
+ }
}
diff --git a/src/com/android/bitmap/BitmapCache.java b/src/com/android/bitmap/BitmapCache.java
index d671c17..fc76c3a 100644
--- a/src/com/android/bitmap/BitmapCache.java
+++ b/src/com/android/bitmap/BitmapCache.java
@@ -19,4 +19,7 @@
public interface BitmapCache extends PooledCache<DecodeTask.Request, ReusableBitmap> {
void setBlocking(boolean blocking);
+ void setPoolDimensions(int decodeWidth, int decodeHeight);
+ int getDecodeWidth();
+ int getDecodeHeight();
}
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
index 2210ec6..d7a3cab 100644
--- a/src/com/android/bitmap/DecodeTask.java
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -54,6 +54,7 @@
public interface Request {
AssetFileDescriptor createFd() throws IOException;
InputStream createInputStream() throws IOException;
+ boolean hasOrientationExif() throws IOException;
}
/**
@@ -65,8 +66,6 @@
* may have been preempted by the scheduler or queued up by a bottlenecked executor.
* <p>
* N.B. this method runs on the UI thread.
- *
- * @param key
*/
void onDecodeBegin(Request key);
void onDecodeComplete(Request key, ReusableBitmap result);
@@ -86,13 +85,17 @@
@Override
protected ReusableBitmap doInBackground(Void... params) {
+ // enqueue the 'onDecodeBegin' signal on the main thread
+ publishProgress();
+
+ return decode();
+ }
+
+ public ReusableBitmap decode() {
if (isCancelled()) {
return null;
}
- // enqueue the 'onDecodeBegin' signal on the main thread
- publishProgress();
-
ReusableBitmap result = null;
AssetFileDescriptor fd = null;
InputStream in = null;
@@ -130,20 +133,26 @@
Trace.endSection();
Trace.beginSection("get orientation");
- if (fd != null) {
- // Creating an input stream from the file descriptor makes it useless afterwards.
- Trace.beginSection("create fd and stream");
- final AssetFileDescriptor orientationFd = mKey.createFd();
- in = orientationFd.createInputStream();
- Trace.endSection();
- }
- final int orientation = Exif.getOrientation(in, byteSize);
- if (fd != null) {
- try {
- // Close the temporary file descriptor.
- in.close();
- } catch (IOException ex) {
+ final int orientation;
+ if (mKey.hasOrientationExif()) {
+ if (fd != null) {
+ // Creating an input stream from the file descriptor makes it useless
+ // afterwards.
+ Trace.beginSection("create fd and stream");
+ final AssetFileDescriptor orientationFd = mKey.createFd();
+ in = orientationFd.createInputStream();
+ Trace.endSection();
}
+ orientation = Exif.getOrientation(in, byteSize);
+ if (fd != null) {
+ try {
+ // Close the temporary file descriptor.
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ } else {
+ orientation = 0;
}
final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
Trace.endSection();
@@ -246,6 +255,7 @@
}
}
+ //noinspection PointlessBooleanExpression
if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
try {
Trace.beginSection("decode" + mOpts.inSampleSize);
@@ -307,13 +317,13 @@
if (fd != null) {
try {
fd.close();
- } catch (IOException e) {
+ } catch (IOException ignored) {
}
}
if (in != null) {
try {
in.close();
- } catch (IOException e) {
+ } catch (IOException ignored) {
}
}
if (result != null) {
@@ -388,7 +398,7 @@
} else {
try {
in.close();
- } catch (IOException ex) {
+ } catch (IOException ignored) {
}
in = mKey.createInputStream();
}
@@ -414,6 +424,7 @@
// round to the nearest power of two, or just truncate
final boolean stricter = true;
+ //noinspection ConstantConditions
if (stricter) {
result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
} else {
diff --git a/src/com/android/bitmap/PooledCache.java b/src/com/android/bitmap/PooledCache.java
index 62381b8..6d6684f 100644
--- a/src/com/android/bitmap/PooledCache.java
+++ b/src/com/android/bitmap/PooledCache.java
@@ -24,4 +24,14 @@
V poll();
String toDebugString();
+ /**
+ * Purge existing Poolables from the pool+cache. Usually, this is done when situations
+ * change and the items in the pool+cache are no longer appropriate. For example,
+ * if the layout changes, the pool+cache may need to hold larger bitmaps.
+ *
+ * <p/>
+ * The existing Poolables will be garbage collected when they are no longer being referenced
+ * by other objects.
+ */
+ void clear();
}
diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java
index 2e0199f..dde9bd1 100644
--- a/src/com/android/bitmap/ReusableBitmap.java
+++ b/src/com/android/bitmap/ReusableBitmap.java
@@ -115,4 +115,37 @@
return sb.toString();
}
+ /**
+ * Singleton class to represent a null Bitmap. We don't want to just use a regular
+ * ReusableBitmap with a null bmp field because that will render that ReusableBitmap useless
+ * and unable to be used by another decode process.
+ */
+ public final static class NullReusableBitmap extends ReusableBitmap {
+ private static NullReusableBitmap sInstance;
+
+ /**
+ * Get a singleton.
+ */
+ public static NullReusableBitmap getInstance() {
+ if (sInstance == null) {
+ sInstance = new NullReusableBitmap();
+ }
+ return sInstance;
+ }
+
+ private NullReusableBitmap() {
+ super(null /* bmp */, false /* reusable */);
+ }
+
+ @Override
+ public int getByteCount() {
+ return 0;
+ }
+
+ @Override
+ public void releaseReference() { }
+
+ @Override
+ public void acquireReference() { }
+ }
}
diff --git a/src/com/android/mail/ContactInfo.java b/src/com/android/mail/ContactInfo.java
index b7cecb8..a59dc62 100644
--- a/src/com/android/mail/ContactInfo.java
+++ b/src/com/android/mail/ContactInfo.java
@@ -47,6 +47,6 @@
@Override
public String toString() {
- return "{status=" + status + " photo=" + photo + "}";
+ return "{status=" + status + " photo=" + (photo != null ? photo : photoBytes) + "}";
}
}
diff --git a/src/com/android/mail/SenderInfoLoader.java b/src/com/android/mail/SenderInfoLoader.java
index 5d01ad4..c69e934 100644
--- a/src/com/android/mail/SenderInfoLoader.java
+++ b/src/com/android/mail/SenderInfoLoader.java
@@ -17,6 +17,7 @@
package com.android.mail;
+import com.android.bitmap.Trace;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
@@ -99,40 +100,53 @@
/**
* Loads contact photos from the ContentProvider.
* @param resolver {@link ContentResolver} to use in queries to the ContentProvider.
- * @param senderSet The email addresses of the sender images to return.
+ * @param emails The email addresses of the sender images to return.
* @param decodeBitmaps If {@code true}, decode the bitmaps and put them into
* {@link ContactInfo}. Otherwise, just put the raw bytes of the photo
* into the {@link ContactInfo}.
- * @return A mapping of email addresses to {@link ContactInfo}s. The {@link ContactInfo} will
- * contain either a byte array or an actual decoded bitmap for the sender image.
+ * @return A mapping of email to {@link ContactInfo}. How to interpret the map:
+ * <ul>
+ * <li>The email is missing from the key set or maps to null - The email was skipped. Try
+ * again.</li>
+ * <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null -
+ * Photo loaded successfully.</li>
+ * <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null -
+ * Photo load failed.</li>
+ * </ul>
*/
public static ImmutableMap<String, ContactInfo> loadContactPhotos(
- final ContentResolver resolver, final Set<String> senderSet,
- final boolean decodeBitmaps) {
+ final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) {
+ Trace.beginSection("load contact photos util");
Cursor cursor = null;
+ Trace.beginSection("build first query");
Map<String, ContactInfo> results = Maps.newHashMap();
// temporary structures
Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap();
ArrayList<String> photoIdsAsStrings = new ArrayList<String>();
- ArrayList<String> senders = getTruncatedQueryParams(senderSet);
+ ArrayList<String> emailsList = getTruncatedQueryParams(emails);
// Build first query
StringBuilder query = new StringBuilder()
.append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE)
.append("' AND ").append(Email.DATA).append(" IN (");
- appendQuestionMarks(query, senders);
+ appendQuestionMarks(query, emailsList);
query.append(')');
+ Trace.endSection();
try {
+ Trace.beginSection("query 1");
cursor = resolver.query(Data.CONTENT_URI, DATA_COLS,
- query.toString(), toStringArray(senders), null /* sortOrder */);
+ query.toString(), toStringArray(emailsList), null /* sortOrder */);
+ Trace.endSection();
if (cursor == null) {
+ Trace.endSection();
return null;
}
+ Trace.beginSection("get photo id");
int i = -1;
while (cursor.moveToPosition(++i)) {
String email = cursor.getString(DATA_EMAIL_COLUMN);
@@ -154,11 +168,23 @@
results.put(email, result);
}
cursor.close();
+ Trace.endSection();
+
+ // Put empty ContactInfo for all the emails that didn't map to a contact.
+ // This allows us to differentiate between lookup failed,
+ // and lookup skipped (truncated above).
+ for (String email : emailsList) {
+ if (!results.containsKey(email)) {
+ results.put(email, new ContactInfo(null, null));
+ }
+ }
if (photoIdsAsStrings.isEmpty()) {
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
+ Trace.beginSection("build second query");
// Build second query: photoIDs->blobs
// based on photo batch-select code in ContactPhotoManager
photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings);
@@ -166,14 +192,19 @@
query.append(Photo._ID).append(" IN (");
appendQuestionMarks(query, photoIdsAsStrings);
query.append(')');
+ Trace.endSection();
+ Trace.beginSection("query 2");
cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS,
query.toString(), toStringArray(photoIdsAsStrings), null /* sortOrder */);
+ Trace.endSection();
if (cursor == null) {
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
+ Trace.beginSection("get photo blob");
i = -1;
while (cursor.moveToPosition(++i)) {
byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN);
@@ -187,7 +218,9 @@
ContactInfo prevResult = prev.second;
if (decodeBitmaps) {
+ Trace.beginSection("decode bitmap");
Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
+ Trace.endSection();
// overwrite existing photo-less result
results.put(email,
new ContactInfo(prevResult.contactUri, prevResult.status, photo));
@@ -197,12 +230,14 @@
prevResult.contactUri, prevResult.status, photoBytes));
}
}
+ Trace.endSection();
} finally {
if (cursor != null) {
cursor.close();
}
}
+ Trace.endSection();
return ImmutableMap.copyOf(results);
}
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
index 0252455..49e4008 100644
--- a/src/com/android/mail/bitmap/AttachmentDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -147,11 +147,13 @@
// requests for different renditions of the same attachment
final boolean onlyRenditionChange = (mCurrKey != null && mCurrKey.matches(key));
+ Trace.beginSection("release reference");
if (mBitmap != null && !onlyRenditionChange) {
mBitmap.releaseReference();
// System.out.println("view.bind() decremented ref to old bitmap: " + mBitmap);
mBitmap = null;
}
+ Trace.endSection();
if (mCurrKey != null && SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
mDecodeAggregator.forget(mCurrKey);
}
@@ -169,6 +171,7 @@
setLoadState(LOAD_STATE_UNINITIALIZED);
if (key == null) {
+ invalidateSelf();
Trace.endSection();
return;
}
@@ -200,7 +203,7 @@
return;
}
- if (mBitmap != null) {
+ if (mBitmap != null && mBitmap.bmp != null) {
BitmapUtils
.calculateCroppedSrcRect(mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
bounds.width(), bounds.height(),
diff --git a/src/com/android/mail/bitmap/AttachmentGridDrawable.java b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
index f5d28ed..c1e5789 100644
--- a/src/com/android/mail/bitmap/AttachmentGridDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
@@ -2,6 +2,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
+import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
@@ -47,7 +48,7 @@
}
@Override
- protected AttachmentDrawable createDivisionDrawable() {
+ protected AttachmentDrawable createDivisionDrawable(final int i) {
final AttachmentDrawable result = new AttachmentDrawable(mResources, mCache,
mDecodeAggregator, mCoordinates, mPlaceholder, mProgress);
return result;
@@ -105,8 +106,24 @@
}
@Override
+ public void setAlpha(final int alpha) {
+ super.setAlpha(alpha);
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ super.setColorFilter(cf);
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ @Override
public void setParallaxFraction(float fraction) {
mParallaxFraction = fraction;
}
-
}
diff --git a/src/com/android/mail/bitmap/CompositeDrawable.java b/src/com/android/mail/bitmap/CompositeDrawable.java
index e25bfc7..e372bb5 100644
--- a/src/com/android/mail/bitmap/CompositeDrawable.java
+++ b/src/com/android/mail/bitmap/CompositeDrawable.java
@@ -21,11 +21,13 @@
public abstract class CompositeDrawable<T extends Drawable> extends Drawable
implements Drawable.Callback {
+ public static final int MAX_COMPOSITE_DRAWABLES = 4;
+
protected final List<T> mDrawables;
protected int mCount;
public CompositeDrawable(int maxDivisions) {
- if (maxDivisions >= 4) {
+ if (maxDivisions > MAX_COMPOSITE_DRAWABLES) {
throw new IllegalArgumentException("CompositeDrawable only supports 4 divisions");
}
mDrawables = new ArrayList<T>(maxDivisions);
@@ -35,7 +37,7 @@
mCount = 0;
}
- protected abstract T createDivisionDrawable();
+ protected abstract T createDivisionDrawable(final int i);
public void setCount(int count) {
// zero out the composite bounds, which will propagate to the division drawables
@@ -56,7 +58,7 @@
T result = mDrawables.get(i);
if (result == null) {
Trace.beginSection("create division drawable");
- result = createDivisionDrawable();
+ result = createDivisionDrawable(i);
mDrawables.set(i, result);
result.setCallback(this);
// Make sure drawables created after the bounds were already set have their bounds
@@ -109,6 +111,11 @@
@Override
public void draw(Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
for (int i = 0; i < mCount; i++) {
mDrawables.get(i).draw(canvas);
}
@@ -132,10 +139,7 @@
public int getOpacity() {
int opacity = PixelFormat.OPAQUE;
for (int i = 0; i < mCount; i++) {
- if (mDrawables.get(i).getOpacity() != PixelFormat.OPAQUE) {
- opacity = PixelFormat.TRANSLUCENT;
- break;
- }
+ opacity = resolveOpacity(opacity, mDrawables.get(i).getOpacity());
}
return opacity;
}
diff --git a/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
new file mode 100644
index 0000000..134439d
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2013 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.mail.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R.color;
+import com.android.mail.R.drawable;
+
+/**
+ * Custom FlipDrawable which has a {@link ContactGridDrawable} on the front,
+ * and a {@link CheckmarkDrawable} on the back.
+ */
+public class ContactCheckableGridDrawable extends FlipDrawable implements AnimatorUpdateListener {
+
+ private final ContactGridDrawable mContactGridDrawable;
+ private final CheckmarkDrawable mCheckmarkDrawable;
+
+ private final ValueAnimator mCheckmarkScaleAnimator;
+ private final ValueAnimator mCheckmarkAlphaAnimator;
+
+ private static final int POST_FLIP_DURATION_MS = 150;
+
+ private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+ private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+ /** Must be <= 1f since the animation value is used as a percentage. */
+ private static final float END_VALUE = 1f;
+
+ public ContactCheckableGridDrawable(final Resources res, final int flipDurationMs) {
+ super(new ContactGridDrawable(res), new CheckmarkDrawable(res), flipDurationMs,
+ 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+ mContactGridDrawable = (ContactGridDrawable) mFront;
+ mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+ // We will create checkmark animations that are synchronized with the flipping animation.
+ // The entire delay + duration of the checkmark animation needs to equal the entire
+ // duration of the flip animation (where delay is 0).
+
+ // The checkmark animation is in effect only when the back drawable is being shown.
+ // For the flip animation duration <pre>[_][]|[][_]<post>
+ // The checkmark animation will be |--delay--|-duration-|
+
+ // Need delay to skip the first half of the flip duration.
+ final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+ // Actual duration is the second half of the flip duration.
+ final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+ mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+ mCheckmarkScaleAnimator.addUpdateListener(this);
+
+ mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+ mCheckmarkAlphaAnimator.addUpdateListener(this);
+ }
+
+ @Override
+ public void reset(final boolean side) {
+ super.reset(side);
+ if (mCheckmarkScaleAnimator == null) {
+ // Call from super's constructor. Not yet initialized.
+ return;
+ }
+ mCheckmarkScaleAnimator.cancel();
+ mCheckmarkAlphaAnimator.cancel();
+ mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+ mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+ }
+
+ @Override
+ public void flip() {
+ super.flip();
+ // Keep the checkmark animators in sync with the flip animator.
+ if (mCheckmarkScaleAnimator.isStarted()) {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ } else {
+ if (!getSideFlippingTowards() /* front to back */) {
+ mCheckmarkScaleAnimator.start();
+ mCheckmarkAlphaAnimator.start();
+ } else /* back to front */ {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ }
+ }
+ }
+
+ public ContactDrawable getOrCreateDrawable(final int i) {
+ return mContactGridDrawable.getOrCreateDrawable(i);
+ }
+
+ public void setBitmapCache(final BitmapCache cache) {
+ mContactGridDrawable.setBitmapCache(cache);
+ }
+
+ public void setContactResolver(final ContactResolver contactResolver) {
+ mContactGridDrawable.setContactResolver(contactResolver);
+ }
+
+ public int getCount() {
+ return mContactGridDrawable.getCount();
+ }
+
+ public void setCount(final int count) {
+ mContactGridDrawable.setCount(count);
+ // Side effect needs to happen here too.
+ setBounds(0, 0, 0, 0);
+ }
+
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ //noinspection ConstantConditions
+ final float value = (Float) animation.getAnimatedValue();
+
+ if (animation == mCheckmarkScaleAnimator) {
+ mCheckmarkDrawable.setScaleAnimatorValue(value);
+ } else if (animation == mCheckmarkAlphaAnimator) {
+ mCheckmarkDrawable.setAlphaAnimatorValue(value);
+ }
+ }
+
+ /**
+ * Meant to be used as the with a FlipDrawable. The animator driving this Drawable should be
+ * more or less in sync with the containing FlipDrawable's flip animator.
+ */
+ private static class CheckmarkDrawable extends Drawable {
+
+ private static Bitmap CHECKMARK;
+ private static int sBackgroundColor;
+
+ private final Paint mPaint;
+
+ private float mScaleFraction;
+ private float mAlphaFraction;
+
+ private static final Matrix sMatrix = new Matrix();
+
+ public CheckmarkDrawable(final Resources res) {
+ if (CHECKMARK == null) {
+ CHECKMARK = BitmapFactory.decodeResource(res, drawable.ic_avatar_check);
+ sBackgroundColor = res.getColor(color.checkmark_tile_background_color);
+ }
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ mPaint.setColor(sBackgroundColor);
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ canvas.drawRect(getBounds(), mPaint);
+
+ // Scale the checkmark.
+ sMatrix.reset();
+ sMatrix.setScale(mScaleFraction, mScaleFraction, CHECKMARK.getWidth() / 2,
+ CHECKMARK.getHeight() / 2);
+ sMatrix.postTranslate(bounds.centerX() - CHECKMARK.getWidth() / 2,
+ bounds.centerY() - CHECKMARK.getHeight() / 2);
+
+ // Fade the checkmark.
+ final int oldAlpha = mPaint.getAlpha();
+ // Interpolate the alpha.
+ mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+ canvas.drawBitmap(CHECKMARK, sMatrix, mPaint);
+ // Restore the alpha.
+ mPaint.setAlpha(oldAlpha);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ // Always a gray background.
+ return PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setScaleAnimatorValue(final float value) {
+ final float old = mScaleFraction;
+ mScaleFraction = value;
+ if (old != mScaleFraction) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setAlphaAnimatorValue(final float value) {
+ final float old = mAlphaFraction;
+ mAlphaFraction = value;
+ if (old != mAlphaFraction) {
+ invalidateSelf();
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactDrawable.java b/src/com/android/mail/bitmap/ContactDrawable.java
new file mode 100644
index 0000000..34aa682
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactDrawable.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2013 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.mail.bitmap;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap;
+import com.android.mail.R;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a contact image,
+ * including request creation/cancelling and data unbinding/re-binding. While no contact images
+ * can be shown, a default letter tile will be shown instead.
+ *
+ * <p/>
+ * The actual contact resolving and decoding is handled by {@link ContactResolver}.
+ */
+public class ContactDrawable extends Drawable {
+
+ private final BitmapCache mCache;
+ private final ContactResolver mContactResolver;
+
+ private ContactRequest mContactRequest;
+ private ReusableBitmap mBitmap;
+ private final Paint mPaint;
+ private int mScale;
+
+ /** Letter tile */
+ private static TypedArray sColors;
+ private static int sDefaultColor;
+ private static int sTileLetterFontSize;
+ private static int sTileLetterFontSizeSmall;
+ private static int sTileFontColor;
+ private static Bitmap DEFAULT_AVATAR;
+ /** Reusable components to avoid new allocations */
+ private static final Paint sPaint = new Paint();
+ private static final Rect sRect = new Rect();
+ private static final char[] sFirstChar = new char[1];
+
+ /** This should match the total number of colors defined in colors.xml for letter_tile_color */
+ private static final int NUM_OF_TILE_COLORS = 8;
+
+ public ContactDrawable(final Resources res, final BitmapCache cache,
+ final ContactResolver contactResolver) {
+ mCache = cache;
+ mContactResolver = contactResolver;
+ mPaint = new Paint();
+ mPaint.setFilterBitmap(true);
+ mPaint.setDither(true);
+
+ if (sColors == null) {
+ sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+ sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+ sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
+ sTileLetterFontSizeSmall = res
+ .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+ sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+ DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
+
+ sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ if (mBitmap != null && mBitmap.bmp != null) {
+ // Draw sender image.
+ drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas);
+ } else {
+ // Draw letter tile.
+ drawLetterTile(canvas);
+ }
+ }
+
+ /**
+ * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+ */
+ private void drawBitmap(final Bitmap bitmap, final int width, final int height,
+ final Canvas canvas) {
+ final Rect bounds = getBounds();
+
+ if (mScale != ContactGridDrawable.SCALE_TYPE_HALF) {
+ sRect.set(0, 0, width, height);
+ } else {
+ // For skinny bounds, draw the middle two quarters.
+ sRect.set(width / 4, 0, width / 4 * 3, height);
+ }
+ canvas.drawBitmap(bitmap, sRect, bounds, mPaint);
+ }
+
+ private void drawLetterTile(final Canvas canvas) {
+ if (mContactRequest == null) {
+ return;
+ }
+
+ // Draw background color.
+ final String email = mContactRequest.getEmail();
+ sPaint.setColor(pickColor(email));
+ sPaint.setAlpha(mPaint.getAlpha());
+ canvas.drawRect(getBounds(), sPaint);
+
+ // Draw letter/digit or generic avatar.
+ final String displayName = mContactRequest.getDisplayName();
+ final char firstChar = displayName.charAt(0);
+ final Rect bounds = getBounds();
+ if (isEnglishLetterOrDigit(firstChar)) {
+ // Draw letter or digit.
+ sFirstChar[0] = Character.toUpperCase(firstChar);
+ sPaint.setTextSize(mScale == ContactGridDrawable.SCALE_TYPE_ONE ? sTileLetterFontSize
+ : sTileLetterFontSizeSmall);
+ sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+ sPaint.setColor(sTileFontColor);
+ canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+ bounds.centerY() + sRect.height() / 2, sPaint);
+ } else {
+ drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(),
+ canvas);
+ }
+ }
+
+ private static int pickColor(final String email) {
+ // String.hashCode() implementation is not supposed to change across java versions, so
+ // this should guarantee the same email address always maps to the same color.
+ // The email should already have been normalized by the ContactRequest.
+ final int color = Math.abs(email.hashCode()) % NUM_OF_TILE_COLORS;
+ return sColors.getColor(color, sDefaultColor);
+ }
+
+ private static boolean isEnglishLetterOrDigit(final char c) {
+ return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9');
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) {
+ mCache.setPoolDimensions(decodeWidth, decodeHeight);
+ }
+
+ public void setScale(final int scale) {
+ mScale = scale;
+ }
+
+ public void unbind() {
+ setImage(null);
+ }
+
+ public void bind(final String name, final String email) {
+ setImage(new ContactRequest(name, email));
+ }
+
+ private void setImage(final ContactRequest contactRequest) {
+ if (mContactRequest != null && mContactRequest.equals(contactRequest)) {
+ return;
+ }
+
+ if (mBitmap != null) {
+ mBitmap.releaseReference();
+ mBitmap = null;
+ }
+
+ mContactResolver.remove(mContactRequest, this);
+ mContactRequest = contactRequest;
+
+ if (contactRequest == null) {
+ invalidateSelf();
+ return;
+ }
+
+ final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */);
+ if (cached != null) {
+ setBitmap(cached);
+ } else {
+ decode();
+ }
+ }
+
+ private void setBitmap(final ReusableBitmap bmp) {
+ if (mBitmap != null && mBitmap != bmp) {
+ mBitmap.releaseReference();
+ }
+ mBitmap = bmp;
+ invalidateSelf();
+ }
+
+ private void decode() {
+ if (mContactRequest == null) {
+ return;
+ }
+ // Add to batch.
+ mContactResolver.add(mContactRequest, this);
+ }
+
+ public void onDecodeComplete(final Request key, final ReusableBitmap result) {
+ final ContactRequest request = (ContactRequest) key;
+ // Remove from batch.
+ mContactResolver.remove(request, this);
+ if (request.equals(mContactRequest)) {
+ setBitmap(result);
+ } else {
+ // if the requests don't match (i.e. this request is stale), decrement the
+ // ref count to allow the bitmap to be pooled
+ if (result != null) {
+ result.releaseReference();
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactGridDrawable.java b/src/com/android/mail/bitmap/ContactGridDrawable.java
new file mode 100644
index 0000000..590f806
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactGridDrawable.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2013 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.mail.bitmap;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R;
+
+/**
+ * A 2x2 grid of contact drawables. Adds horizontal and vertical dividers.
+ */
+public class ContactGridDrawable extends CompositeDrawable<ContactDrawable> {
+
+ public static final int SCALE_TYPE_ONE = 0;
+ public static final int SCALE_TYPE_HALF = 1;
+ public static final int SCALE_TYPE_QUARTER = 2;
+
+ private static final int MAX_CONTACTS_COUNT = 4;
+
+ private ContactResolver mContactResolver;
+ private BitmapCache mCache;
+ private final Resources mRes;
+ private Paint mPaint;
+
+ private static int sDividerWidth = -1;
+ private static int sDividerColor;
+
+ public ContactGridDrawable(final Resources res) {
+ super(MAX_CONTACTS_COUNT);
+
+ if (sDividerWidth == -1) {
+ sDividerWidth = res.getDimensionPixelSize(R.dimen.tile_divider_width);
+ sDividerColor = res.getColor(R.color.tile_divider_color);
+ }
+
+ mRes = res;
+ mPaint = new Paint();
+ mPaint.setStrokeWidth(sDividerWidth);
+ mPaint.setColor(sDividerColor);
+ }
+
+ @Override
+ protected ContactDrawable createDivisionDrawable(final int i) {
+ final ContactDrawable drawable = new ContactDrawable(mRes, mCache, mContactResolver);
+ drawable.setScale(calculateScale(i));
+ return drawable;
+ }
+
+ @Override
+ public void setCount(final int count) {
+ super.setCount(count);
+
+ for (int i = 0; i < mCount; i++) {
+ final ContactDrawable drawable = mDrawables.get(i);
+ if (drawable != null) {
+ drawable.setScale(calculateScale(i));
+ }
+ }
+ }
+
+ /**
+ * Given which section a drawable is in, calculate its scale based on the current total count.
+ * @param i The section, indexed by 0.
+ */
+ private int calculateScale(final int i) {
+ switch (mCount) {
+ case 1:
+ // 1 bitmap: passthrough
+ return SCALE_TYPE_ONE;
+ case 2:
+ // 2 bitmaps split vertically
+ return SCALE_TYPE_HALF;
+ case 3:
+ // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+ return i == 0 ? SCALE_TYPE_HALF : SCALE_TYPE_QUARTER;
+ case 4:
+ // 4 bitmaps in a 2x2 grid
+ return SCALE_TYPE_QUARTER;
+ default:
+ return SCALE_TYPE_ONE;
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ super.draw(canvas);
+
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ // Draw horizontal and vertical dividers.
+ switch (mCount) {
+ case 1:
+ // 1 bitmap: passthrough
+ break;
+ case 2:
+ // 2 bitmaps split vertically
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ break;
+ case 3:
+ // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ canvas.drawLine(bounds.centerX(), bounds.centerY(), bounds.right, bounds.centerY(),
+ mPaint);
+ break;
+ case 4:
+ // 4 bitmaps in a 2x2 grid
+ canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+ mPaint);
+ canvas.drawLine(bounds.left, bounds.centerY(), bounds.right, bounds.centerY(),
+ mPaint);
+ break;
+ }
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ super.setAlpha(alpha);
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ super.setColorFilter(cf);
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ public void setBitmapCache(final BitmapCache cache) {
+ mCache = cache;
+ }
+
+ public void setContactResolver(final ContactResolver contactResolver) {
+ mContactResolver = contactResolver;
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactRequest.java b/src/com/android/mail/bitmap/ContactRequest.java
new file mode 100644
index 0000000..b0bd2a0
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactRequest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2013 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.mail.bitmap;
+
+import android.content.res.AssetFileDescriptor;
+import android.text.TextUtils;
+
+import com.android.bitmap.DecodeTask;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A request object for contact images. ContactRequests have a destination because multiple
+ * ContactRequests can share the same decoded data.
+ */
+public class ContactRequest implements DecodeTask.Request {
+
+ private final String mName;
+ private final String mEmail;
+
+ public byte[] bytes;
+
+ public ContactRequest(final String name, final String email) {
+ mName = name;
+ mEmail = normalizeEmail(email);
+ }
+
+ private String normalizeEmail(final String email) {
+ if (TextUtils.isEmpty(email)) {
+ throw new IllegalArgumentException("Email must not be empty.");
+ }
+ // todo: b/10258788
+ return email;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ContactRequest that = (ContactRequest) o;
+
+ // Only count email, so we can pull results out of the cache that are from other contact
+ // requests.
+ //noinspection RedundantIfStatement
+ if (mEmail != null ? !mEmail.equals(that.mEmail) : that.mEmail != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ // Only count email, so we can pull results out of the cache that are from other contact
+ // requests.
+ return mEmail != null ? mEmail.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + super.toString() + " mName=" + mName + " mEmail=" + mEmail + "]";
+ }
+
+ @Override
+ public AssetFileDescriptor createFd() throws IOException {
+ return null;
+ }
+
+ @Override
+ public InputStream createInputStream() throws IOException {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public boolean hasOrientationExif() throws IOException {
+ return false;
+ }
+
+ public String getEmail() {
+ return mEmail;
+ }
+
+ public String getDisplayName() {
+ return !TextUtils.isEmpty(mName) ? mName : mEmail;
+ }
+
+ /**
+ * This ContactRequest wrapper provides implementations of equals() and hashcode() that
+ * include the destination. We need to put multiple ContactRequests in a set,
+ * but its implementations of equals() and hashcode() don't include the destination.
+ */
+ public static class ContactRequestHolder {
+
+ public final ContactRequest contactRequest;
+ public final ContactDrawable destination;
+
+ public ContactRequestHolder(final ContactRequest contactRequest,
+ final ContactDrawable destination) {
+ this.contactRequest = contactRequest;
+ this.destination = destination;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ContactRequestHolder that = (ContactRequestHolder) o;
+
+ if (contactRequest != null ? !contactRequest.equals(that.contactRequest)
+ : that.contactRequest != null) {
+ return false;
+ }
+ //noinspection RedundantIfStatement
+ if (destination != null ? !destination.equals(that.destination)
+ : that.destination != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = contactRequest != null ? contactRequest.hashCode() : 0;
+ result = 31 * result + (destination != null ? destination.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return contactRequest.toString();
+ }
+
+ public String getEmail() {
+ return contactRequest.getEmail();
+ }
+
+ public String getDisplayName() {
+ return contactRequest.getDisplayName();
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/ContactResolver.java b/src/com/android/mail/bitmap/ContactResolver.java
new file mode 100644
index 0000000..b3d1fd5
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactResolver.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to 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.mail.bitmap;
+
+import android.content.ContentResolver;
+import android.os.AsyncTask;
+import android.os.AsyncTask.Status;
+import android.os.Handler;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask;
+import com.android.bitmap.ReusableBitmap;
+import com.android.ex.photo.util.Trace;
+import com.android.mail.ContactInfo;
+import com.android.mail.SenderInfoLoader;
+import com.android.mail.bitmap.ContactRequest.ContactRequestHolder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Batches up ContactRequests so we can efficiently query the contacts provider. Kicks off a
+ * ContactResolverTask to query for contact images in the background.
+ */
+public class ContactResolver implements Runnable {
+
+ private static final String TAG = LogTag.getLogTag();
+
+ private final ContentResolver mResolver;
+ private final BitmapCache mCache;
+ /** Insertion ordered set allows us to work from the top down. */
+ private final LinkedHashSet<ContactRequestHolder> mBatch;
+
+ private final Handler mHandler = new Handler();
+ private ContactResolverTask mTask;
+
+
+ /** Size 1 pool mostly to make systrace output traces on one line. */
+ private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
+ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
+
+ public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
+ mResolver = resolver;
+ mCache = cache;
+ mBatch = new LinkedHashSet<ContactRequestHolder>();
+ }
+
+ @Override
+ public void run() {
+ // Start to process a new batch.
+ if (mBatch.isEmpty()) {
+ return;
+ }
+
+ if (mTask != null && mTask.getStatus() == Status.RUNNING) {
+ LogUtils.d(TAG, "ContactResolver << batch skip");
+ return;
+ }
+
+ Trace.beginSection("ContactResolver run");
+ LogUtils.d(TAG, "ContactResolver >> batch start");
+
+ // Make a copy of the batch.
+ LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
+
+ if (mTask != null) {
+ mTask.cancel(true);
+ }
+
+ mTask = new ContactResolverTask(batch, mResolver, mCache, this);
+ mTask.executeOnExecutor(EXECUTOR);
+ Trace.endSection();
+ }
+
+ public void add(final ContactRequest request, final ContactDrawable drawable) {
+ mBatch.add(new ContactRequestHolder(request, drawable));
+ notifyBatchReady();
+ }
+
+ public void remove(final ContactRequest request, final ContactDrawable drawable) {
+ mBatch.remove(new ContactRequestHolder(request, drawable));
+ }
+
+ /**
+ * A layout pass traverses the whole tree during a single iteration of the event loop. That
+ * means that every ContactDrawable on the screen will add its ContactRequest to the batch in
+ * a single iteration of the event loop.
+ *
+ * <p/>
+ * We take advantage of this by posting a Runnable (happens to be this object) at the end of
+ * the event queue. Every time something is added to the batch as part of the same layout pass,
+ * the Runnable is moved to the back of the queue. When the next layout pass occurs,
+ * it is placed in the event loop behind this Runnable. That allows us to process the batch
+ * that was added previously.
+ */
+ private void notifyBatchReady() {
+ LogUtils.d(TAG, "ContactResolver > batch %d", mBatch.size());
+ mHandler.removeCallbacks(this);
+ mHandler.post(this);
+ }
+
+ /**
+ * This is not a very traditional AsyncTask, in the sense that we do not care about what gets
+ * returned in doInBackground(). Instead, we signal traditional "return values" through
+ * publishProgress().
+ *
+ * <p/>
+ * The reason we do this is because this task is responsible for decoding an entire batch of
+ * ContactRequests. But, we do not want to have to wait to decode all of them before updating
+ * any views. So we must do all the work in doInBackground(),
+ * but upon finishing each individual task, we need to jump out to the UI thread and update
+ * that view.
+ */
+ private static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
+
+ private final Set<ContactRequestHolder> mContactRequests;
+ private final ContentResolver mResolver;
+ private final BitmapCache mCache;
+ private final ContactResolver mCallback;
+
+ public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
+ final ContentResolver resolver, final BitmapCache cache,
+ final ContactResolver callback) {
+ mContactRequests = contactRequests;
+ mResolver = resolver;
+ mCache = cache;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Void doInBackground(final Void... params) {
+ Trace.beginSection("set up");
+ final Set<String> emails = new HashSet<String>(mContactRequests.size());
+ for (ContactRequestHolder request : mContactRequests) {
+ final String email = request.getEmail();
+ emails.add(email);
+ }
+ Trace.endSection();
+
+ Trace.beginSection("load contact photo bytes");
+ // Query the contacts provider for the current batch of emails.
+ ImmutableMap<String, ContactInfo> contactInfos = SenderInfoLoader
+ .loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
+ Trace.endSection();
+
+ for (ContactRequestHolder request : mContactRequests) {
+ Trace.beginSection("decode");
+ final String email = request.getEmail();
+ if (contactInfos == null) {
+ // Query failed.
+ LogUtils.d(TAG, "ContactResolver -- failed %s", email);
+ publishProgress(new Result(request, null));
+ Trace.endSection();
+ continue;
+ }
+
+ final ContactInfo contactInfo = contactInfos.get(email);
+ if (contactInfo == null) {
+ // Request skipped. Try again next batch.
+ LogUtils.d(TAG, "ContactResolver = skipped %s", email);
+ Trace.endSection();
+ continue;
+ }
+
+ // Query attempted.
+ final byte[] photo = contactInfo.photoBytes;
+ if (photo == null) {
+ // No photo bytes found.
+ LogUtils.d(TAG, "ContactResolver -- failed %s", email);
+ publishProgress(new Result(request, null));
+ Trace.endSection();
+ continue;
+ }
+
+ // Query succeeded. Photo bytes found.
+ request.contactRequest.bytes = photo;
+
+ // Start decode.
+ LogUtils.d(TAG, "ContactResolver ++ found %s", email);
+ final ReusableBitmap result;
+ final int width = mCache.getDecodeWidth();
+ final int height = mCache.getDecodeHeight();
+ // Synchronously decode the photo bytes. We are already in a background
+ // thread, and we want decodes to finish in order. The decodes are blazing
+ // fast so we don't need to kick off multiple threads.
+ result = new DecodeTask(request.contactRequest, width, height, width, height, null,
+ mCache).decode();
+ request.contactRequest.bytes = null;
+
+ // Decode success.
+ publishProgress(new Result(request, result));
+ Trace.endSection();
+ }
+
+ return null;
+ }
+
+ /**
+ * We use progress updates to jump to the UI thread so we can decode the batch
+ * incrementally.
+ */
+ @Override
+ protected void onProgressUpdate(final Result... values) {
+ final ContactRequestHolder request = values[0].request;
+ final ReusableBitmap bitmap = values[0].bitmap;
+
+ // DecodeTask does not add null results to the cache.
+ if (bitmap == null) {
+ // Cache null result.
+ mCache.put(request.contactRequest, null);
+ }
+
+ request.destination.onDecodeComplete(request.contactRequest, bitmap);
+ }
+
+ @Override
+ protected void onPostExecute(final Void aVoid) {
+ // Batch completed. Start next batch.
+ mCallback.notifyBatchReady();
+ }
+ }
+
+ /**
+ * Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
+ * to onProgressUpdate().
+ */
+ private static class Result {
+ public final ContactRequestHolder request;
+ public final ReusableBitmap bitmap;
+
+ private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
+ this.request = request;
+ this.bitmap = bitmap;
+ }
+ }
+}
diff --git a/src/com/android/mail/bitmap/FlipDrawable.java b/src/com/android/mail/bitmap/FlipDrawable.java
new file mode 100644
index 0000000..6cc7b26
--- /dev/null
+++ b/src/com/android/mail/bitmap/FlipDrawable.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2013 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.mail.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * <pre>[_][]|[][_]<post>
+ * | | |
+ * V V V
+ * <pre>< flip ><post>
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+ /**
+ * The inner drawables.
+ */
+ protected final Drawable mFront;
+ protected final Drawable mBack;
+
+ protected final int mFlipDurationMs;
+ protected final int mPreFlipDurationMs;
+ protected final int mPostFlipDurationMs;
+ private final ValueAnimator mFlipAnimator;
+
+ private static final float END_VALUE = 2f;
+
+ /**
+ * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
+ * mFront is fully shown, while END_VALUE means mBack is fully shown.
+ */
+ private float mFlipFraction = 0f;
+
+ /**
+ * True if flipping towards front, false if flipping towards back.
+ */
+ private boolean mFlipToSide = true;
+
+ /**
+ * Create a new FlipDrawable. The front is fully shown by default.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * <pre>[_][]|[][_]<post>
+ * | | |
+ * V V V
+ * <pre>< flip ><post>
+ * </pre>
+ *
+ * @param front The front drawable.
+ * @param back The back drawable.
+ * @param flipDurationMs The duration of the actual flip. This duration includes both
+ * animating away one side and showing the other.
+ * @param preFlipDurationMs The duration before the actual flip begins. Subclasses can use this
+ * to add flourish.
+ * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
+ * to add flourish.
+ */
+ public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
+ final int preFlipDurationMs, final int postFlipDurationMs) {
+ if (front == null || back == null) {
+ throw new IllegalArgumentException("Front and back drawables must not be null.");
+ }
+ mFront = front;
+ mBack = back;
+
+ mFront.setCallback(this);
+ mBack.setCallback(this);
+
+ mFlipDurationMs = flipDurationMs;
+ mPreFlipDurationMs = preFlipDurationMs;
+ mPostFlipDurationMs = postFlipDurationMs;
+
+ mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
+ .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
+ mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ final float old = mFlipFraction;
+ //noinspection ConstantConditions
+ mFlipFraction = (Float) animation.getAnimatedValue();
+ if (old != mFlipFraction) {
+ invalidateSelf();
+ }
+ }
+ });
+
+ reset(true);
+ }
+
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+ if (bounds.isEmpty()) {
+ mFront.setBounds(0, 0, 0, 0);
+ mBack.setBounds(0, 0, 0, 0);
+ } else {
+ mFront.setBounds(bounds);
+ mBack.setBounds(bounds);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
+
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+
+ final float scaleX;
+ if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
+ // During pre-flip.
+ scaleX = 1;
+ } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
+ // During post-flip.
+ scaleX = 1;
+ } else {
+ // During flip.
+ final float flipFraction = mFlipFraction / 2;
+ final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ final float distFraction = Math.abs(flipFraction - flipMiddle);
+ final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
+ scaleX = distFraction * multiplier;
+ }
+
+ canvas.save();
+ // The flip is a simple 1 dimensional scale.
+ canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
+ inner.draw(canvas);
+ canvas.restore();
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mFront.setAlpha(alpha);
+ mBack.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mFront.setColorFilter(cf);
+ mBack.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
+ }
+
+ @Override
+ protected boolean onLevelChange(final int level) {
+ return mFront.setLevel(level) || mBack.setLevel(level);
+ }
+
+ @Override
+ public void invalidateDrawable(final Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(final Drawable who, final Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ /**
+ * Stop animating the flip and reset to one side.
+ * @param side Pass true if reset to front, false if reset to back.
+ */
+ public void reset(final boolean side) {
+ final float old = mFlipFraction;
+ mFlipAnimator.cancel();
+ mFlipFraction = side ? 0f : 2f;
+ mFlipToSide = side;
+ if (mFlipFraction != old) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Returns true if the front is shown. Returns false if the back is shown.
+ */
+ public boolean getSideShown() {
+ final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+ final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+ + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+ return mFlipFraction / 2 < middleFraction;
+ }
+
+ /**
+ * Returns true if the front is being flipped towards. Returns false if the back is being
+ * flipped towards.
+ */
+ public boolean getSideFlippingTowards() {
+ return mFlipToSide;
+ }
+
+ /**
+ * Starts an animated flip to the other side. If a flip animation is currently started,
+ * it will be reversed.
+ */
+ public void flip() {
+ mFlipToSide = !mFlipToSide;
+ if (mFlipAnimator.isStarted()) {
+ mFlipAnimator.reverse();
+ } else {
+ if (!mFlipToSide /* front to back */) {
+ mFlipAnimator.start();
+ } else /* back to front */ {
+ mFlipAnimator.reverse();
+ }
+ }
+ }
+
+ /**
+ * Start an animated flip to a side. This works regardless of whether a flip animation is
+ * currently started.
+ * @param side Pass true if flip to front, false if flip to back.
+ */
+ public void flipTo(final boolean side) {
+ if (mFlipToSide != side) {
+ flip();
+ }
+ }
+
+ /**
+ * Returns whether flipping is in progress.
+ */
+ public boolean isFlipping() {
+ return mFlipAnimator.isStarted();
+ }
+}
diff --git a/src/com/android/mail/bitmap/ImageAttachmentRequest.java b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
index 6c58772..b680db5 100644
--- a/src/com/android/mail/bitmap/ImageAttachmentRequest.java
+++ b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
@@ -1,6 +1,5 @@
package com.android.mail.bitmap;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
@@ -25,6 +24,9 @@
private final int mRendition;
public final int mDestW;
+ private Uri mCachedUri;
+ private String mCachedMimeType;
+
public ImageAttachmentRequest(final Context context, final String lookupUri,
final int rendition, final int destW) {
mContext = context;
@@ -78,26 +80,40 @@
@Override
public AssetFileDescriptor createFd() throws IOException {
- AssetFileDescriptor result = null;
+ if (mCachedUri == null) {
+ cacheValues();
+ }
+ return mContext.getContentResolver().openAssetFileDescriptor(mCachedUri, "r");
+ }
+
+ private void cacheValues() throws IOException {
Cursor cursor = null;
- final ContentResolver cr = mContext.getContentResolver();
try {
- cursor = cr.query(Uri.parse(mLookupUri), UIProvider.ATTACHMENT_PROJECTION, null, null,
- null);
+ cursor = mContext.getContentResolver().query(Uri.parse(mLookupUri),
+ UIProvider.ATTACHMENT_PROJECTION, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
final Attachment a = new Attachment(cursor);
- result = cr.openAssetFileDescriptor(a.getUriForRendition(mRendition), "r");
+ mCachedUri = a.getUriForRendition(mRendition);
+ final String mimeType = a.getContentType();
+ mCachedMimeType = mimeType != null ? mimeType.toLowerCase() : null;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
- return result;
}
@Override
public InputStream createInputStream() throws IOException {
return null;
}
+
+ @Override
+ public boolean hasOrientationExif() throws IOException {
+ if (mCachedUri == null) {
+ cacheValues();
+ }
+ return mCachedMimeType == null || mCachedMimeType.equals("image/jpeg");
+ }
}
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index 6c15e88..7a3ca0f 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -165,6 +165,8 @@
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final boolean mCachingEnabled;
+
private void setCursor(UnderlyingCursorWrapper cursor) {
// If we have an existing underlying cursor, make sure it's closed
if (mUnderlyingCursor != null) {
@@ -193,6 +195,9 @@
mName = name;
qProjection = UIProvider.CONVERSATION_PROJECTION;
mCursorObserver = new CursorObserver(new Handler(Looper.getMainLooper()));
+
+ // Disable caching on low memory devices
+ mCachingEnabled = !Utils.isLowRamDevice(activity);
}
/**
@@ -357,7 +362,7 @@
* notes on thread safety.
*/
private int mCachePos;
- private boolean mCachingEnabled = true;
+ private boolean mCachingEnabled;
private final NewCursorUpdateObserver mCursorUpdateObserver;
private boolean mUpdateObserverRegistered = false;
@@ -370,9 +375,11 @@
private boolean mCursorUpdated = false;
- public UnderlyingCursorWrapper(Cursor result) {
+ public UnderlyingCursorWrapper(Cursor result, boolean cachingEnabled) {
super(result);
+ mCachingEnabled = cachingEnabled;
+
// Register the content observer immediately, as we want to make sure that we don't miss
// any updates
mCursorUpdateObserver =
@@ -639,7 +646,8 @@
uri, time, result.getCount());
}
System.gc();
- return new UnderlyingCursorWrapper(result);
+
+ return new UnderlyingCursorWrapper(result, mCachingEnabled);
}
static boolean offUiThread() {
@@ -1946,6 +1954,15 @@
}
@Override
+ public Uri getNotificationUri() {
+ if (mUnderlyingCursor == null) {
+ return null;
+ } else {
+ return mUnderlyingCursor.getNotificationUri();
+ }
+ }
+
+ @Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
throw new UnsupportedOperationException();
}
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 94d5dfc..5dd2267 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -18,8 +18,6 @@
package com.android.mail.browse;
import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.ClipData;
@@ -31,7 +29,6 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
-import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
@@ -60,7 +57,6 @@
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.DecelerateInterpolator;
-import android.view.animation.LinearInterpolator;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
@@ -72,11 +68,10 @@
import com.android.mail.analytics.Analytics;
import com.android.mail.bitmap.AttachmentDrawable;
import com.android.mail.bitmap.AttachmentGridDrawable;
+import com.android.mail.bitmap.ContactCheckableGridDrawable;
+import com.android.mail.bitmap.ContactDrawable;
import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
import com.android.mail.perf.Timer;
-import com.android.mail.photomanager.ContactPhotoManager;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
import com.android.mail.providers.Address;
import com.android.mail.providers.Attachment;
import com.android.mail.providers.Conversation;
@@ -87,9 +82,9 @@
import com.android.mail.providers.UIProvider.ConversationListIcon;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
+import com.android.mail.ui.ConversationSetObserver;
import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
@@ -102,13 +97,12 @@
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
import java.util.ArrayList;
-import java.util.List;
public class ConversationItemView extends View
- implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
+ implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener,
+ ConversationSetObserver {
// Timer.
private static int sLayoutCount = 0;
@@ -125,7 +119,6 @@
// Static bitmaps.
private static Bitmap STAR_OFF;
private static Bitmap STAR_ON;
- private static Bitmap CHECK;
private static Bitmap ATTACHMENT;
private static Bitmap ONLY_TO_ME;
private static Bitmap TO_ME_AND_OTHERS;
@@ -200,15 +193,12 @@
private float mAnimatedHeightFraction = 1.0f;
private final String mAccount;
private ControllableActivity mActivity;
- private ConversationListListener mConversationListListener;
private final TextView mSubjectTextView;
private final TextView mSendersTextView;
private int mGadgetMode;
private boolean mAttachmentPreviewsEnabled;
private boolean mParallaxSpeedAlternative;
private boolean mParallaxDirectionAlternative;
- private final DividedImageCanvas mContactImagesHolder;
- private static ContactPhotoManager sContactPhotoManager;
private static int sFoldersLeftPadding;
private static TextAppearanceSpan sSubjectTextUnreadSpan;
@@ -218,19 +208,9 @@
private static int sScrollSlop;
private static CharacterStyle sActivatedTextSpan;
+ private final ContactCheckableGridDrawable mSendersImageView;
private final AttachmentGridDrawable mAttachmentsView;
- private final Matrix mPhotoFlipMatrix = new Matrix();
- private final Matrix mCheckMatrix = new Matrix();
-
- private final CabAnimator mPhotoFlipAnimator;
-
- /**
- * The conversation id, if this conversation was selected the last time we were in a selection
- * mode. This is reset after any animations complete upon exiting the selection mode.
- */
- private long mLastSelectedId = -1;
-
/** The resource id of the color to use to override the background. */
private int mBackgroundOverrideResId = -1;
/** The bitmap to use, or <code>null</code> for the default */
@@ -258,19 +238,6 @@
sCheckBackgroundPaint.setColor(Color.GRAY);
}
- public static void setScrollStateChanged(final int scrollState) {
- if (sContactPhotoManager == null) {
- return;
- }
- final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
-
- if (flinging) {
- sContactPhotoManager.pause();
- } else {
- sContactPhotoManager.resume();
- }
- }
-
/**
* Handles displaying folders in a conversation header view.
*/
@@ -421,7 +388,6 @@
// Initialize static bitmaps.
STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
- CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check);
ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
@@ -467,24 +433,10 @@
sElidedPaddingToken = res.getString(R.string.elided_padding_token);
sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
- sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
- sCabAnimationDuration =
- res.getInteger(R.integer.conv_item_view_cab_anim_duration);
+ sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
}
- mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2,
- sCabAnimationDuration) {
- @Override
- public void invalidateArea() {
- final int left = mCoordinates.contactImagesX;
- final int right = left + mContactImagesHolder.getWidth();
- final int top = mCoordinates.contactImagesY;
- final int bottom = top + mContactImagesHolder.getHeight();
- invalidate(left, top, right, bottom);
- }
- };
-
mSendersTextView = new TextView(mContext);
mSendersTextView.setIncludeFontPadding(false);
@@ -492,27 +444,16 @@
mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
mSubjectTextView.setIncludeFontPadding(false);
- mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() {
- @Override
- public void invalidate() {
- if (mCoordinates == null) {
- return;
- }
- ConversationItemView.this.invalidate(mCoordinates.contactImagesX,
- mCoordinates.contactImagesY,
- mCoordinates.contactImagesX + mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
- }
- });
-
mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
mAttachmentsView.setCallback(this);
+ mSendersImageView = new ContactCheckableGridDrawable(res, sCabAnimationDuration);
+ mSendersImageView.setCallback(this);
+
Utils.traceEndSection();
}
public void bind(final Conversation conversation, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
@@ -520,31 +461,28 @@
final AnimatedAdapter adapter) {
Utils.traceBeginSection("CIVC.bind");
bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
- conversationListListener, null /* conversationItemAreaClickListener */, set, folder,
- checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
- parallaxDirectionAlternative, swipeEnabled, priorityArrowEnabled, adapter,
- -1 /* backgroundOverrideResId */,
- null /* photoBitmap */);
+ null /* conversationItemAreaClickListener */,
+ set, folder, checkboxOrSenderImage, showAttachmentPreviews,
+ parallaxSpeedAlternative, parallaxDirectionAlternative, swipeEnabled,
+ priorityArrowEnabled, adapter, -1 /* backgroundOverrideResId */, null /* photoBitmap */);
Utils.traceEndSection();
}
public void bindAd(final ConversationItemViewModel conversationItemViewModel,
final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationItemAreaClickListener conversationItemAreaClickListener,
final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
final int backgroundOverrideResId, final Bitmap photoBitmap) {
Utils.traceBeginSection("CIVC.bindAd");
- bind(conversationItemViewModel, activity, conversationListListener,
- conversationItemAreaClickListener, null /* set */, folder, checkboxOrSenderImage,
- false /* attachment previews */, false /* parallax */, false /* parallax */,
- true /* swipeEnabled */, false /* priorityArrowEnabled */, adapter,
- backgroundOverrideResId, photoBitmap);
+ bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
+ folder, checkboxOrSenderImage, false /* attachment previews */,
+ false /* parallax */, false /* parallax */, true /* swipeEnabled */,
+ false /* priorityArrowEnabled */,
+ adapter, backgroundOverrideResId, photoBitmap);
Utils.traceEndSection();
}
private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationItemAreaClickListener conversationItemAreaClickListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
@@ -556,28 +494,25 @@
mConversationItemAreaClickListener = conversationItemAreaClickListener;
if (mHeader != null) {
+ Utils.traceBeginSection("unbind");
+ final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
// If this was previously bound to a different conversation, remove any contact photo
// manager requests.
- if (header.conversation.id != mHeader.conversation.id ||
- (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames
- .equals(header.displayableSenderNames))) {
- ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
- if (divisionIds != null) {
- mContactImagesHolder.reset();
- for (int pos = 0; pos < divisionIds.size(); pos++) {
- sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash(
- mContactImagesHolder, pos, divisionIds.get(pos)));
- }
+ if (newlyBound || (mHeader.displayableSenderNames != null && !mHeader
+ .displayableSenderNames.equals(
+ header.displayableSenderNames))) {
+ for (int i = 0; i < mSendersImageView.getCount(); i++) {
+ mSendersImageView.getOrCreateDrawable(i).unbind();
}
+ mSendersImageView.setCount(0);
}
// If this was previously bound to a different conversation,
// remove any attachment preview manager requests.
- if (header.conversation.id != mHeader.conversation.id
- || header.conversation.attachmentPreviewsCount
- != mHeader.conversation.attachmentPreviewsCount
- || !header.conversation.getAttachmentPreviewUris()
- .equals(mHeader.conversation.getAttachmentPreviewUris())) {
+ if (newlyBound || header.conversation.attachmentPreviewsCount
+ != mHeader.conversation.attachmentPreviewsCount || !header.conversation
+ .getAttachmentPreviewUris().equals(
+ mHeader.conversation.getAttachmentPreviewUris())) {
// unbind the attachments view (releasing bitmap references)
// (this also cancels all async tasks)
@@ -588,22 +523,29 @@
mAttachmentsView.setCount(0);
}
- if (header.conversation.id != mHeader.conversation.id) {
+ if (newlyBound) {
// Stop the photo flip animation
- mPhotoFlipAnimator.stopAnimation();
+ final boolean showSenders = !isSelected();
+ mSendersImageView.reset(showSenders);
}
+ Utils.traceEndSection();
}
mCoordinates = null;
mHeader = header;
mActivity = activity;
- mConversationListListener = conversationListListener;
mSelectedConversationSet = set;
+ mSelectedConversationSet.addObserver(this);
mDisplayedFolder = folder;
mStarEnabled = folder != null && !folder.isTrash();
mSwipeEnabled = swipeEnabled;
mAdapter = adapter;
- mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
- mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator());
+
+ Utils.traceBeginSection("drawables");
+ mAttachmentsView.setBitmapCache(mAdapter.getAttachmentPreviewsCache());
+ mAttachmentsView.setDecodeAggregator(mAdapter.getAttachmentPreviewsDecodeAggregator());
+ mSendersImageView.setBitmapCache(mAdapter.getSendersImagesCache());
+ mSendersImageView.setContactResolver(mAdapter.getContactResolver());
+ Utils.traceEndSection();
if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
@@ -615,12 +557,14 @@
mParallaxSpeedAlternative = parallaxSpeedAlternative;
mParallaxDirectionAlternative = parallaxDirectionAlternative;
+ Utils.traceBeginSection("folder displayer");
// Initialize folder displayer.
if (mHeader.folderDisplayer == null) {
mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
} else {
mHeader.folderDisplayer.reset();
}
+ Utils.traceEndSection();
final int ignoreFolderType;
if (mDisplayedFolder.isInbox()) {
@@ -629,16 +573,21 @@
ignoreFolderType = -1;
}
+ Utils.traceBeginSection("load folders");
mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
mDisplayedFolder.folderUri, ignoreFolderType);
+ Utils.traceEndSection();
if (mHeader.dateOverrideText == null) {
+ Utils.traceBeginSection("relative time");
mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
mHeader.conversation.dateMs);
+ Utils.traceEndSection();
} else {
mHeader.dateText = mHeader.dateOverrideText;
}
+ Utils.traceBeginSection("config setup");
mConfig = new ConversationItemViewCoordinates.Config()
.withGadget(mGadgetMode)
.withAttachmentPreviews(getAttachmentPreviewsMode());
@@ -651,6 +600,7 @@
if (mHeader.conversation.color != 0) {
mConfig.showColorBlock();
}
+
// Personal level.
mHeader.personalLevelBitmap = null;
if (true) { // TODO: hook this up to a setting
@@ -672,15 +622,20 @@
if (mHeader.personalLevelBitmap != null) {
mConfig.showPersonalIndicator();
}
+ Utils.traceEndSection();
+ Utils.traceBeginSection("overflow");
mAttachmentsView.setOverflowText(null);
+ Utils.traceEndSection();
+ Utils.traceBeginSection("content description");
setContentDescription();
+ Utils.traceEndSection();
requestLayout();
}
@Override
- public void invalidateDrawable(Drawable who) {
+ public void invalidateDrawable(final Drawable who) {
boolean handled = false;
if (mCoordinates != null) {
if (mAttachmentsView.equals(who)) {
@@ -688,6 +643,11 @@
r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
handled = true;
+ } else if (mSendersImageView.equals(who)) {
+ final Rect r = new Rect(who.getBounds());
+ r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
+ ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
+ handled = true;
}
}
if (!handled) {
@@ -761,12 +721,14 @@
Utils.traceEndSection();
// Subject.
+ Utils.traceBeginSection("subject");
createSubject(mHeader.unread);
if (!mHeader.isLayoutValid()) {
setContentDescription();
}
mHeader.validate();
+ Utils.traceEndSection();
pauseTimer(PERF_TAG_LAYOUT);
if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
@@ -916,34 +878,36 @@
// FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
// is immutable.
private void loadSenderImages() {
- if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
- && mHeader.displayableSenderEmails != null
- && mHeader.displayableSenderEmails.size() > 0) {
- if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
- LogUtils.w(LOG_TAG,
- "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
- mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
- mCoordinates.getMode());
- return;
- }
-
- int size = mHeader.displayableSenderEmails.size();
- final List<Object> keys = Lists.newArrayListWithCapacity(size);
- for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
- keys.add(mHeader.displayableSenderEmails.get(i));
- }
-
- mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesHeight);
- mContactImagesHolder.setDivisionIds(keys);
- String emailAddress;
- for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
- emailAddress = mHeader.displayableSenderEmails.get(i);
- PhotoIdentifier photoIdentifier = new ContactIdentifier(
- mHeader.displayableSenderNames.get(i), emailAddress, i);
- sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
- }
+ if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
+ || mHeader.displayableSenderEmails == null
+ || mHeader.displayableSenderEmails.size() <= 0) {
+ return;
}
+ if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
+ LogUtils.w(LOG_TAG,
+ "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
+ mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
+ mCoordinates.getMode());
+ return;
+ }
+
+ Utils.traceBeginSection("load sender images");
+ final int count = mHeader.displayableSenderEmails.size();
+
+ mSendersImageView.setCount(count);
+ mSendersImageView
+ .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
+
+ for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < count; i++) {
+ Utils.traceBeginSection("load single sender image");
+ final ContactDrawable drawable = mSendersImageView.getOrCreateDrawable(i);
+ drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
+ mCoordinates.contactImagesHeight);
+ drawable.bind(mHeader.displayableSenderNames.get(i),
+ mHeader.displayableSenderEmails.get(i));
+ Utils.traceEndSection();
+ }
+ Utils.traceEndSection();
}
private void loadAttachmentPreviews() {
@@ -1391,7 +1355,9 @@
// Contact photo
if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
canvas.save();
- drawContactImageArea(canvas);
+ Utils.traceBeginSection("draw senders image");
+ drawSendersImage(canvas);
+ Utils.traceEndSection();
canvas.restore();
}
@@ -1500,113 +1466,19 @@
Utils.traceEndSection();
}
- /**
- * Draws the contact images or check, in the correct animated state.
- */
- private void drawContactImageArea(final Canvas canvas) {
- if (isSelected()) {
- mLastSelectedId = mHeader.conversation.id;
-
- // Since this is selected, we draw the checkbox if the animation is not running, or if
- // it's running, and is past the half-way point
- if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) {
- // Flash in the check
- drawCheckbox(canvas);
- } else {
- // Flip out the contact photo
- drawContactImages(canvas);
- }
- } else {
- if ((mConversationListListener.isExitingSelectionMode()
- && mLastSelectedId == mHeader.conversation.id)
- || mPhotoFlipAnimator.isStarted()) {
- // Animate back to the photo
- if (!mPhotoFlipAnimator.isStarted()) {
- mPhotoFlipAnimator.startAnimation(true /* reverse */);
- }
-
- if (mPhotoFlipAnimator.getValue() > 1) {
- // Flash out the check
- drawCheckbox(canvas);
- } else {
- // Flip in the contact photo
- drawContactImages(canvas);
- }
- } else {
- mLastSelectedId = -1; // We don't care anymore
- mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state
-
- // Contact photos
- drawContactImages(canvas);
- }
+ private void drawSendersImage(final Canvas canvas) {
+ if (!mSendersImageView.isFlipping()) {
+ final boolean showSenders = !isSelected();
+ mSendersImageView.reset(showSenders);
}
- }
-
- private void drawContactImages(final Canvas canvas) {
- // mPhotoFlipFraction goes from 0 to 1
- final float value = mPhotoFlipAnimator.getValue();
-
- final float scale = 1f - value;
- final float xOffset = mContactImagesHolder.getWidth() * value / 2;
-
- mPhotoFlipMatrix.reset();
- mPhotoFlipMatrix.postScale(scale, 1);
-
- final float x = mCoordinates.contactImagesX + xOffset;
- final float y = mCoordinates.contactImagesY;
-
- canvas.translate(x, y);
-
+ canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
if (mPhotoBitmap == null) {
- mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
+ mSendersImageView.draw(canvas);
} else {
canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
}
}
- private void drawCheckbox(final Canvas canvas) {
- // mPhotoFlipFraction goes from 1 to 2
-
- // Draw the background
- canvas.save();
- canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
- canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
- sCheckBackgroundPaint);
- canvas.restore();
-
- final int x = mCoordinates.contactImagesX
- + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2;
- final int y = mCoordinates.contactImagesY
- + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2;
-
- final float value = mPhotoFlipAnimator.getValue();
- final float scale;
-
- if (!mPhotoFlipAnimator.isStarted()) {
- // We're not animating
- scale = 1;
- } else if (value < 1.9) {
- // 1.0 to 1.9 will scale 0 to 1
- scale = (value - 1f) / 0.9f;
- } else if (value < 1.95) {
- // 1.9 to 1.95 will scale 1 to 19/18
- scale = (value - 1f) / 0.9f;
- } else {
- // 1.95 to 2.0 will scale 19/18 to 1
- scale = (0.95f - (value - 1.95f)) / 0.9f;
- }
-
- final float xOffset = CHECK.getWidth() * (1f - scale) / 2f;
- final float yOffset = CHECK.getHeight() * (1f - scale) / 2f;
-
- mCheckMatrix.reset();
- mCheckMatrix.postScale(scale, scale);
-
- canvas.translate(x + xOffset, y + yOffset);
-
- canvas.drawBitmap(CHECK, mCheckMatrix, sPaint);
- }
-
private void drawAttachmentPreviews(Canvas canvas) {
canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
final float fraction;
@@ -1685,13 +1557,13 @@
return toggleSelectedState(null);
}
- private boolean toggleSelectedState(String sourceOpt) {
+ private boolean toggleSelectedState(final String sourceOpt) {
if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
mSelected = !mSelected;
setSelected(mSelected);
- Conversation conv = mHeader.conversation;
+ final Conversation conv = mHeader.conversation;
// Set the list position of this item in the conversation
- SwipeableListView listView = getListView();
+ final SwipeableListView listView = getListView();
try {
conv.position = mSelected && listView != null ? listView.getPositionForView(this)
@@ -1710,9 +1582,8 @@
listView.commitDestructiveActions(true);
}
- final boolean reverse = !mSelected;
- mPhotoFlipAnimator.startAnimation(reverse);
- mPhotoFlipAnimator.invalidateArea();
+ final boolean front = !mSelected;
+ mSendersImageView.flipTo(front);
// We update the background after the checked state has changed
// now that we have a selected background asset. Setting the background
@@ -1726,6 +1597,17 @@
return false;
}
+ @Override
+ public void onSetEmpty() {
+ mSendersImageView.flipTo(true);
+ }
+
+ @Override
+ public void onSetPopulated(final ConversationSelectionSet set) { }
+
+ @Override
+ public void onSetChanged(final ConversationSelectionSet set) { }
+
/**
* Toggle the star on this view and update the conversation.
*/
@@ -2173,124 +2055,6 @@
return sScrollSlop;
}
- private abstract class CabAnimator {
- private ObjectAnimator mAnimator = null;
-
- private final String mPropertyName;
-
- private float mValue;
-
- private final float mStartValue;
- private final float mEndValue;
-
- private final long mDuration;
-
- private boolean mReversing = false;
-
- public CabAnimator(final String propertyName, final float startValue, final float endValue,
- final long duration) {
- mPropertyName = propertyName;
-
- mStartValue = startValue;
- mEndValue = endValue;
-
- mDuration = duration;
- }
-
- private ObjectAnimator createAnimator() {
- final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this,
- mPropertyName, mStartValue, mEndValue);
- animator.setDuration(mDuration);
- animator.setInterpolator(new LinearInterpolator());
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator animation) {
- invalidateArea();
- }
- });
- animator.addListener(mAnimatorListener);
- return animator;
- }
-
- private final AnimatorListener mAnimatorListener = new AnimatorListener() {
- @Override
- public void onAnimationStart(final Animator animation) {
- // Do nothing
- }
-
- @Override
- public void onAnimationEnd(final Animator animation) {
- if (mReversing) {
- mReversing = false;
- // We no longer want to track whether we were last selected,
- // since we no longer are selected
- mLastSelectedId = -1;
- }
- }
-
- @Override
- public void onAnimationCancel(final Animator animation) {
- // Do nothing
- }
-
- @Override
- public void onAnimationRepeat(final Animator animation) {
- // Do nothing
- }
- };
-
- public abstract void invalidateArea();
-
- public void setValue(final float fraction) {
- if (mValue == fraction) {
- return;
- }
- mValue = fraction;
- invalidateArea();
- }
-
- public float getValue() {
- return mValue;
- }
-
- /**
- * @param reverse <code>true</code> to animate in reverse
- */
- public void startAnimation(final boolean reverse) {
- if (mAnimator != null) {
- mAnimator.cancel();
- }
-
- mAnimator = createAnimator();
- mReversing = reverse;
-
- if (reverse) {
- mAnimator.reverse();
- } else {
- mAnimator.start();
- }
- }
-
- public void stopAnimation() {
- if (mAnimator != null) {
- mAnimator.cancel();
- mAnimator = null;
- }
-
- mReversing = false;
-
- setValue(0);
- }
-
- public boolean isStarted() {
- return mAnimator != null && mAnimator.isStarted();
- }
- }
-
- public void setPhotoFlipFraction(final float fraction) {
- mPhotoFlipAnimator.setValue(fraction);
- }
-
public String getAccount() {
return mAccount;
}
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index cd17c2f..919a5aa 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -28,7 +28,6 @@
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
@@ -56,15 +55,14 @@
}
public void bind(final Conversation conversation, final ControllableActivity activity,
- final ConversationListListener conversationListListener,
final ConversationSelectionSet set, final Folder folder,
final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
final boolean swipeEnabled, final boolean priorityArrowsEnabled,
final AnimatedAdapter animatedAdapter) {
- mConversationItemView.bind(conversation, activity, conversationListListener, set, folder,
- checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
- parallaxDirectionAlternative, swipeEnabled, priorityArrowsEnabled, animatedAdapter);
+ mConversationItemView.bind(conversation, activity, set, folder, checkboxOrSenderImage,
+ showAttachmentPreviews, parallaxSpeedAlternative, parallaxDirectionAlternative,
+ swipeEnabled, priorityArrowsEnabled, animatedAdapter);
}
public void startUndoAnimation(AnimatorListener listener, boolean swipe) {
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
deleted file mode 100644
index 06184b0..0000000
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2012 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.mail.photomanager;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.LruCache;
-
-import com.android.mail.ContactInfo;
-import com.android.mail.SenderInfoLoader;
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableMap;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Asynchronously loads contact photos and maintains a cache of photos.
- */
-public class ContactPhotoManager extends PhotoManager {
- public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
-
- /**
- * An LRU cache for photo ids mapped to contact addresses.
- */
- private final LruCache<String, Long> mPhotoIdCache;
- private final LetterTileProvider mLetterTileProvider;
-
- /** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */
- private static final int PHOTO_ID_CACHE_SIZE = 500;
-
- /**
- * Requests the singleton instance with data bound from the available authenticators. This
- * method can safely be called from the UI thread.
- */
- public static ContactPhotoManager getInstance(Context context) {
- Context applicationContext = context.getApplicationContext();
- ContactPhotoManager service =
- (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
- if (service == null) {
- service = createContactPhotoManager(applicationContext);
- LogUtils.e(TAG, "No contact photo service in context: " + applicationContext);
- }
- return service;
- }
-
- public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
- return new ContactPhotoManager(context);
- }
-
- public static int generateHash(ImageCanvas view, int pos, Object key) {
- return Objects.hashCode(view, pos, key);
- }
-
- private ContactPhotoManager(Context context) {
- super(context);
- mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE);
- mLetterTileProvider = new LetterTileProvider(context);
- }
-
- @Override
- protected DefaultImageProvider getDefaultImageProvider() {
- return mLetterTileProvider;
- }
-
- @Override
- protected int getHash(PhotoIdentifier id, ImageCanvas view) {
- final ContactIdentifier contactId = (ContactIdentifier) id;
- return generateHash(view, contactId.pos, contactId.getKey());
- }
-
- @Override
- protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
- return new ContactPhotoLoaderThread(contentResolver);
- }
-
- @Override
- public void clear() {
- super.clear();
- mPhotoIdCache.evictAll();
- }
-
- public static class ContactIdentifier extends PhotoIdentifier {
- public final String name;
- public final String emailAddress;
- public final int pos;
-
- public ContactIdentifier(String name, String emailAddress, int pos) {
- this.name = name;
- this.emailAddress = emailAddress;
- this.pos = pos;
- }
-
- @Override
- public boolean isValid() {
- return !TextUtils.isEmpty(emailAddress);
- }
-
- @Override
- public Object getKey() {
- return emailAddress;
- }
-
- @Override
- public int hashCode() {
- int hash = 17;
- hash = 31 * hash + (emailAddress != null ? emailAddress.hashCode() : 0);
- hash = 31 * hash + (name != null ? name.hashCode() : 0);
- hash = 31 * hash + pos;
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- ContactIdentifier other = (ContactIdentifier) obj;
- return Objects.equal(emailAddress, other.emailAddress)
- && Objects.equal(name, other.name) && Objects.equal(pos, other.pos);
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" name=");
- sb.append(name);
- sb.append(" email=");
- sb.append(emailAddress);
- sb.append(" pos=");
- sb.append(pos);
- sb.append("}");
- return sb.toString();
- }
-
- @Override
- public int compareTo(PhotoIdentifier another) {
- return 0;
- }
- }
-
- public class ContactPhotoLoaderThread extends PhotoLoaderThread {
- public ContactPhotoLoaderThread(ContentResolver resolver) {
- super(resolver);
- }
-
- @Override
- protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) {
- Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(requests.size());
-
- Set<String> addresses = new HashSet<String>();
- Set<Long> photoIds = new HashSet<Long>();
- HashMap<Long, String> photoIdMap = new HashMap<Long, String>();
-
- Long match;
- String emailAddress;
- for (Request request : requests) {
- emailAddress = (String) request.getKey();
- match = mPhotoIdCache.get(emailAddress);
- if (match != null) {
- photoIds.add(match);
- photoIdMap.put(match, emailAddress);
- } else {
- addresses.add(emailAddress);
- }
- }
-
- // get the Map of email addresses to ContactInfo
- ImmutableMap<String, ContactInfo> emailAddressToContactInfoMap =
- SenderInfoLoader.loadContactPhotos(
- getResolver(), addresses, false /* decodeBitmaps */);
-
- // Put all entries into photos map: a mapping of email addresses to photoBytes.
- // If there is no ContactInfo, it means we couldn't get a photo for this
- // address so just put null in for the bytes so that the crazy caching
- // works properly and we don't get an infinite loop of GC churn.
- if (emailAddressToContactInfoMap != null) {
- for (final String address : addresses) {
- final ContactInfo info = emailAddressToContactInfoMap.get(address);
- photos.put(address,
- new BitmapHolder(info != null ? info.photoBytes : null, -1, -1));
- }
- } else {
- // Still need to set a null result for all addresses, otherwise we end
- // up in the loop where photo manager attempts to load these again.
- for (final String address: addresses) {
- photos.put(address, new BitmapHolder(null, -1, -1));
- }
- }
-
- return photos;
- }
- }
-}
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index bc28223..0cb7505 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -29,12 +29,8 @@
import android.text.TextUtils;
import com.android.mail.R;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.DefaultImageProvider;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
-import com.android.mail.ui.DividedImageCanvas;
-import com.android.mail.ui.ImageCanvas;
import com.android.mail.ui.ImageCanvas.Dimensions;
+import com.android.mail.utils.BitmapUtil;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -46,7 +42,8 @@
* tile. If there is no English alphabet character (or digit), it creates a
* bitmap with the default contact avatar.
*/
-public class LetterTileProvider implements DefaultImageProvider {
+@Deprecated
+public class LetterTileProvider {
private static final String TAG = LogTag.getLogTag();
private final Bitmap mDefaultBitmap;
private final Bitmap[] mBitmapBackgroundCache;
@@ -89,30 +86,6 @@
mDefaultColor = res.getColor(R.color.letter_tile_default_color);
}
- @Override
- public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) {
- ContactIdentifier contactIdentifier = (ContactIdentifier) id;
- DividedImageCanvas dividedImageView = (DividedImageCanvas) view;
-
- final String displayName = contactIdentifier.name;
- final String address = (String) contactIdentifier.getKey();
-
- // don't apply again if existing letter is there (and valid)
- if (dividedImageView.hasImageFor(address)) {
- return;
- }
-
- dividedImageView.getDesiredDimensions(address, mDims);
-
- final Bitmap bitmap = getLetterTile(mDims, displayName, address);
-
- if (bitmap == null) {
- return;
- }
-
- dividedImageView.addDivisionImage(bitmap, address);
- }
-
public Bitmap getLetterTile(final Dimensions dimensions, final String displayName,
final String address) {
final String display = !TextUtils.isEmpty(displayName) ? displayName : address;
diff --git a/src/com/android/mail/photomanager/MemInfoReader.java b/src/com/android/mail/photomanager/MemInfoReader.java
deleted file mode 100644
index f53c60c..0000000
--- a/src/com/android/mail/photomanager/MemInfoReader.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2012 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.mail.photomanager;
-
-import android.os.StrictMode;
-
-import java.io.FileInputStream;
-
-public class MemInfoReader {
- byte[] mBuffer = new byte[1024];
-
- private long mTotalSize;
- private long mFreeSize;
- private long mCachedSize;
-
- private static boolean matchText(byte[] buffer, int index, String text) {
- int N = text.length();
- if ((index + N) >= buffer.length) {
- return false;
- }
- for (int i = 0; i < N; i++) {
- if (buffer[index + i] != text.charAt(i)) {
- return false;
- }
- }
- return true;
- }
-
- private static long extractMemValue(byte[] buffer, int index) {
- while (index < buffer.length && buffer[index] != '\n') {
- if (buffer[index] >= '0' && buffer[index] <= '9') {
- int start = index;
- index++;
- while (index < buffer.length && buffer[index] >= '0' && buffer[index] <= '9') {
- index++;
- }
- String str = new String(buffer, 0, start, index - start);
- return ((long) Integer.parseInt(str)) * 1024;
- }
- index++;
- }
- return 0;
- }
-
- public void readMemInfo() {
- // Permit disk reads here, as /proc/meminfo isn't really "on
- // disk" and should be fast. TODO: make BlockGuard ignore
- // /proc/ and /sys/ files perhaps?
- StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- try {
- mTotalSize = 0;
- mFreeSize = 0;
- mCachedSize = 0;
- FileInputStream is = new FileInputStream("/proc/meminfo");
- int len = is.read(mBuffer);
- is.close();
- final int BUFLEN = mBuffer.length;
- int count = 0;
- for (int i = 0; i < len && count < 3; i++) {
- if (matchText(mBuffer, i, "MemTotal")) {
- i += 8;
- mTotalSize = extractMemValue(mBuffer, i);
- count++;
- } else if (matchText(mBuffer, i, "MemFree")) {
- i += 7;
- mFreeSize = extractMemValue(mBuffer, i);
- count++;
- } else if (matchText(mBuffer, i, "Cached")) {
- i += 6;
- mCachedSize = extractMemValue(mBuffer, i);
- count++;
- }
- while (i < BUFLEN && mBuffer[i] != '\n') {
- i++;
- }
- }
- } catch (java.io.FileNotFoundException e) {
- } catch (java.io.IOException e) {
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- public long getTotalSize() {
- return mTotalSize;
- }
-
- public long getFreeSize() {
- return mFreeSize;
- }
-
- public long getCachedSize() {
- return mCachedSize;
- }
-}
diff --git a/src/com/android/mail/photomanager/MemoryUtils.java b/src/com/android/mail/photomanager/MemoryUtils.java
deleted file mode 100644
index 4992026..0000000
--- a/src/com/android/mail/photomanager/MemoryUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2012 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.mail.photomanager;
-
-public class MemoryUtils {
- private MemoryUtils() {
- }
-
- public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
- 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;
- }
-}
diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java
deleted file mode 100644
index 94f809d..0000000
--- a/src/com/android/mail/photomanager/PhotoManager.java
+++ /dev/null
@@ -1,993 +0,0 @@
-/*
- * Copyright (C) 2013 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.mail.photomanager;
-
-import android.content.ComponentCallbacks2;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.os.Process;
-import android.util.LruCache;
-
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
-import com.google.common.base.Objects;
-import com.google.common.collect.Lists;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.PriorityQueue;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Asynchronously loads photos and maintains a cache of photos
- */
-public abstract class PhotoManager implements ComponentCallbacks2, Callback {
- /**
- * Get the default image provider that draws while the photo is being
- * loaded.
- */
- protected abstract DefaultImageProvider getDefaultImageProvider();
-
- /**
- * Generate a hashcode unique to each request.
- */
- protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
-
- /**
- * Return a specific implementation of PhotoLoaderThread.
- */
- protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
-
- /**
- * Subclasses can implement this method to alert callbacks that images finished loading.
- * @param request The original request made.
- * @param success True if we successfully loaded the image from cache. False if we fell back
- * to the default image.
- */
- protected void onImageDrawn(final Request request, final boolean success) {
- // Subclasses can choose to do something about this
- }
-
- /**
- * Subclasses can implement this method to alert callbacks that images started loading.
- * @param request The original request made.
- */
- protected void onImageLoadStarted(final Request request) {
- // Subclasses can choose to do something about this
- }
-
- /**
- * Subclasses can implement this method to determine whether a previously loaded bitmap can
- * be reused for a new canvas size.
- * @param prevWidth The width of the previously loaded bitmap.
- * @param prevHeight The height of the previously loaded bitmap.
- * @param newWidth The width of the canvas this request is drawing on.
- * @param newHeight The height of the canvas this request is drawing on.
- * @return
- */
- protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
- return true;
- }
-
- protected final Context getContext() {
- return mContext;
- }
-
- static final String TAG = "PhotoManager";
- static final boolean DEBUG = false; // Don't submit with true
- static final boolean DEBUG_SIZES = false; // Don't submit with true
-
- private static final String LOADER_THREAD_NAME = "PhotoLoader";
-
- /**
- * Type of message sent by the UI thread to itself to indicate that some photos
- * need to be loaded.
- */
- private static final int MESSAGE_REQUEST_LOADING = 1;
-
- /**
- * Type of message sent by the loader thread to indicate that some photos have
- * been loaded.
- */
- private static final int MESSAGE_PHOTOS_LOADED = 2;
-
- /**
- * Type of message sent by the loader thread to indicate that
- */
- private static final int MESSAGE_PHOTO_LOADING = 3;
-
- public interface DefaultImageProvider {
- /**
- * Applies the default avatar to the DividedImageView. Extent is an
- * indicator for the size (width or height). If darkTheme is set, the
- * avatar is one that looks better on dark background
- * @param id
- */
- public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent);
- }
-
- /**
- * Maintains the state of a particular photo.
- */
- protected static class BitmapHolder {
- byte[] bytes;
- int width;
- int height;
-
- volatile boolean fresh;
-
- public BitmapHolder(byte[] bytes, int width, int height) {
- this.bytes = bytes;
- this.width = width;
- this.height = height;
- this.fresh = true;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" bytes=");
- sb.append(bytes);
- sb.append(" size=");
- sb.append(bytes == null ? 0 : bytes.length);
- sb.append(" width=");
- sb.append(width);
- sb.append(" height=");
- sb.append(height);
- sb.append(" fresh=");
- sb.append(fresh);
- sb.append("}");
- return sb.toString();
- }
- }
-
- // todo:ath caches should be member vars
- /**
- * An LRU cache for bitmap holders. The cache contains bytes for photos just
- * as they come from the database. Each holder has a soft reference to the
- * actual bitmap. The keys are decided by the implementation.
- */
- private static final LruCache<Object, BitmapHolder> sBitmapHolderCache;
-
- /**
- * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
- * the most recently used bitmaps to save time on decoding
- * them from bytes (the bytes are stored in {@link #sBitmapHolderCache}.
- * The keys are decided by the implementation.
- */
- private static final LruCache<BitmapIdentifier, Bitmap> sBitmapCache;
-
- /** Cache size for {@link #sBitmapHolderCache} for devices with "large" RAM. */
- private static final int HOLDER_CACHE_SIZE = 2000000;
-
- /** Cache size for {@link #sBitmapCache} for devices with "large" RAM. */
- private static final int BITMAP_CACHE_SIZE = 1024 * 1024 * 8; // 8MB
-
- /** For debug: How many times we had to reload cached photo for a stale entry */
- private static final AtomicInteger sStaleCacheOverwrite = new AtomicInteger();
-
- /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
- private static final AtomicInteger sFreshCacheOverwrite = new AtomicInteger();
-
- static {
- final float cacheSizeAdjustment =
- (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
- 1.0f : 0.5f;
- final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
- sBitmapHolderCache = 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();
- }
- };
- final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
- sBitmapCache = new LruCache<BitmapIdentifier, Bitmap>(bitmapCacheSize) {
- @Override protected int sizeOf(BitmapIdentifier key, Bitmap value) {
- return value.getByteCount();
- }
-
- @Override protected void entryRemoved(
- boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
- if (DEBUG) dumpStats();
- }
- };
- LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
- if (DEBUG) {
- LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize())
- + " + " + btk(sBitmapCache.maxSize()));
- }
- }
-
- /**
- * A map from ImageCanvas hashcode to the corresponding photo ID or uri,
- * encapsulated in a request. The request may swapped out before the photo
- * loading request is started.
- */
- private final Map<Integer, Request> mPendingRequests = Collections.synchronizedMap(
- new HashMap<Integer, Request>());
-
- /**
- * Handler for messages sent to the UI thread.
- */
- private final Handler mMainThreadHandler = new Handler(this);
-
- /**
- * Thread responsible for loading photos from the database. Created upon
- * the first request.
- */
- private PhotoLoaderThread mLoaderThread;
-
- /**
- * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
- */
- private boolean mLoadingRequested;
-
- /**
- * Flag indicating if the image loading is paused.
- */
- private boolean mPaused;
-
- private final Context mContext;
-
- public PhotoManager(Context context) {
- mContext = context;
- }
-
- public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) {
- loadThumbnail(id, view, null);
- }
-
- /**
- * Load an image
- *
- * @param dimensions Preferred dimensions
- */
- public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view,
- final ImageCanvas.Dimensions dimensions) {
- Utils.traceBeginSection("Load thumbnail");
- final DefaultImageProvider defaultProvider = getDefaultImageProvider();
- final Request request = new Request(id, defaultProvider, view, dimensions);
- final int hashCode = request.hashCode();
-
- if (!id.isValid()) {
- // No photo is needed
- request.applyDefaultImage();
- onImageDrawn(request, false);
- mPendingRequests.remove(hashCode);
- } else if (mPendingRequests.containsKey(hashCode)) {
- LogUtils.d(TAG, "load request dropped for %s", id);
- } else {
- if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey());
- loadPhoto(hashCode, request);
- }
- Utils.traceEndSection();
- }
-
- private void loadPhoto(int hashCode, Request request) {
- if (DEBUG) {
- LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s",
- request.getKey(),
- request,
- Thread.currentThread());
- }
-
- boolean loaded = loadCachedPhoto(request, false);
- if (loaded) {
- if (DEBUG) {
- LogUtils.v(TAG, "image request, cache hit. request queue size=%s",
- mPendingRequests.size());
- }
- } else {
- if (DEBUG) {
- LogUtils.d(TAG, "image request, cache miss: key=%s", request.getKey());
- }
- mPendingRequests.put(hashCode, request);
- if (!mPaused) {
- // Send a request to start loading photos
- requestLoading();
- }
- }
- }
-
- /**
- * Remove photo from the supplied image view. This also cancels current pending load request
- * inside this photo manager.
- */
- public void removePhoto(int hashcode) {
- Request r = mPendingRequests.remove(hashcode);
- if (r != null) {
- LogUtils.d(TAG, "removed request %s", r.getKey());
- }
- }
-
- private void ensureLoaderThread() {
- if (mLoaderThread == null) {
- mLoaderThread = getLoaderThread(mContext.getContentResolver());
- mLoaderThread.start();
- }
- }
-
- /**
- * Checks if the photo is present in cache. If so, sets the photo on the view.
- *
- * @param request Determines which image to load from cache.
- * @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass
- * false if the Loader Thread hasn't made any attempts to
- * load images yet.
- * @return false if the photo needs to be (re)loaded from the provider.
- */
- private boolean loadCachedPhoto(final Request request,
- final boolean afterLoaderThreadFinished) {
- Utils.traceBeginSection("Load cached photo");
- final Bitmap cached = getCachedPhoto(request.bitmapKey);
- if (cached != null) {
- if (DEBUG) {
- LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
- afterLoaderThreadFinished ? "DECODED IMG READ"
- : "DECODED IMG CACHE HIT",
- request.getKey(),
- cached.getByteCount(),
- Thread.currentThread());
- }
- if (request.getView().getGeneration() == request.viewGeneration) {
- request.getView().drawImage(cached, request.getKey());
- onImageDrawn(request, true);
- }
- Utils.traceEndSection();
- return true;
- }
-
- // We couldn't load the requested image, so try to load a replacement.
- // This removes the flicker from SIMPLE to BEST transition.
- final Object replacementKey = request.getPhotoIdentifier().getKeyToShowInsteadOfDefault();
- if (replacementKey != null) {
- final BitmapIdentifier replacementBitmapKey = new BitmapIdentifier(replacementKey,
- request.bitmapKey.w, request.bitmapKey.h);
- final Bitmap cachedReplacement = getCachedPhoto(replacementBitmapKey);
- if (cachedReplacement != null) {
- if (DEBUG) {
- LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
- afterLoaderThreadFinished ? "DECODED IMG READ"
- : "DECODED IMG CACHE HIT",
- replacementKey,
- cachedReplacement.getByteCount(),
- Thread.currentThread());
- }
- if (request.getView().getGeneration() == request.viewGeneration) {
- request.getView().drawImage(cachedReplacement, request.getKey());
- onImageDrawn(request, true);
- }
- Utils.traceEndSection();
- return false;
- }
- }
-
- // We couldn't load any image, so draw a default image
- request.applyDefaultImage();
-
- final BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
- // Check if we loaded null bytes, which means we meant to not draw anything.
- if (holder != null && holder.bytes == null) {
- onImageDrawn(request, holder.fresh);
- Utils.traceEndSection();
- return holder.fresh;
- }
- Utils.traceEndSection();
- return false;
- }
-
- /**
- * Takes care of retrieving the Bitmap from both the decoded and holder caches.
- */
- private static Bitmap getCachedPhoto(BitmapIdentifier bitmapKey) {
- Utils.traceBeginSection("Get cached photo");
- final Bitmap cached = sBitmapCache.get(bitmapKey);
- Utils.traceEndSection();
- return cached;
- }
-
- /**
- * Temporarily stops loading photos from the database.
- */
- public void pause() {
- LogUtils.d(TAG, "%s paused.", getClass().getName());
- mPaused = true;
- }
-
- /**
- * Resumes loading photos from the database.
- */
- public void resume() {
- LogUtils.d(TAG, "%s resumed.", getClass().getName());
- mPaused = false;
- if (DEBUG) dumpStats();
- if (!mPendingRequests.isEmpty()) {
- requestLoading();
- }
- }
-
- /**
- * Sends a message to this thread itself to start loading images. If the current
- * view contains multiple image views, all of those image views will get a chance
- * to request their respective photos before any of those requests are executed.
- * This allows us to load images in bulk.
- */
- private void requestLoading() {
- if (!mLoadingRequested) {
- mLoadingRequested = true;
- mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
- }
- }
-
- /**
- * Processes requests on the main thread.
- */
- @Override
- public boolean handleMessage(final Message msg) {
- switch (msg.what) {
- case MESSAGE_REQUEST_LOADING: {
- mLoadingRequested = false;
- if (!mPaused) {
- ensureLoaderThread();
- mLoaderThread.requestLoading();
- }
- return true;
- }
-
- case MESSAGE_PHOTOS_LOADED: {
- processLoadedImages();
- if (DEBUG) dumpStats();
- return true;
- }
-
- case MESSAGE_PHOTO_LOADING: {
- final int hashcode = msg.arg1;
- final Request request = mPendingRequests.get(hashcode);
- onImageLoadStarted(request);
- return true;
- }
- }
- return false;
- }
-
- /**
- * Goes over pending loading requests and displays loaded photos. If some of the
- * photos still haven't been loaded, sends another request for image loading.
- */
- private void processLoadedImages() {
- Utils.traceBeginSection("process loaded images");
- final List<Integer> toRemove = Lists.newArrayList();
- for (final Integer hash : mPendingRequests.keySet()) {
- final Request request = mPendingRequests.get(hash);
- final boolean loaded = loadCachedPhoto(request, true);
- // Request can go through multiple attempts if the LoaderThread fails to load any
- // images for it, or if the images it loads are evicted from the cache before we
- // could access them in the main thread.
- if (loaded || request.attempts > 2) {
- toRemove.add(hash);
- }
- }
- for (final Integer key : toRemove) {
- mPendingRequests.remove(key);
- }
-
- if (!mPaused && !mPendingRequests.isEmpty()) {
- LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.",
- mPendingRequests.size());
- requestLoading();
- }
- Utils.traceEndSection();
- }
-
- /**
- * Stores the supplied bitmap in cache.
- */
- private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) {
- if (DEBUG) {
- BitmapHolder prev = sBitmapHolderCache.get(cacheKey);
- if (prev != null && prev.bytes != null) {
- LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey
- + (prev.fresh ? " FRESH" : " stale"));
- if (prev.fresh) {
- sFreshCacheOverwrite.incrementAndGet();
- } else {
- sStaleCacheOverwrite.incrementAndGet();
- }
- }
- LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", "
- + (holder.bytes == null ? "<null>" : btk(holder.bytes.length)));
- }
-
- sBitmapHolderCache.put(cacheKey, holder);
- }
-
- protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
- sBitmapCache.put(bitmapKey, bitmap);
- }
-
- // ComponentCallbacks2
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- }
-
- // ComponentCallbacks2
- @Override
- public void onLowMemory() {
- }
-
- // ComponentCallbacks2
- @Override
- public void onTrimMemory(int level) {
- if (DEBUG) LogUtils.d(TAG, "onTrimMemory: " + level);
- if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
- // Clear the caches. Note all pending requests will be removed too.
- clear();
- }
- }
-
- public void clear() {
- if (DEBUG) LogUtils.d(TAG, "clear");
- mPendingRequests.clear();
- sBitmapHolderCache.evictAll();
- sBitmapCache.evictAll();
- }
-
- /**
- * Dump cache stats on logcat.
- */
- private static void dumpStats() {
- if (!DEBUG) {
- return;
- }
- int numHolders = 0;
- int rawBytes = 0;
- int bitmapBytes = 0;
- int numBitmaps = 0;
- for (BitmapHolder h : sBitmapHolderCache.snapshot().values()) {
- numHolders++;
- if (h.bytes != null) {
- rawBytes += h.bytes.length;
- numBitmaps++;
- }
- }
- LogUtils.d(TAG,
- "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
- + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
- + numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numBitmaps)));
- LogUtils.d(TAG, "L1 Stats: %s, overwrite: fresh=%s stale=%s", sBitmapHolderCache,
- sFreshCacheOverwrite.get(), sStaleCacheOverwrite.get());
-
- numBitmaps = 0;
- bitmapBytes = 0;
- for (Bitmap b : sBitmapCache.snapshot().values()) {
- numBitmaps++;
- bitmapBytes += b.getByteCount();
- }
- LogUtils.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: "
- + btk(safeDiv(bitmapBytes, numBitmaps)));
- // We don't get from L2 cache, so L2 stats is meaningless.
- }
-
- /** 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);
- }
-
- public static abstract class PhotoIdentifier implements Comparable<PhotoIdentifier> {
- /**
- * If this returns false, the PhotoManager will not attempt to load the
- * bitmap. Instead, the default image provider will be used.
- */
- public abstract boolean isValid();
-
- /**
- * Identifies this request.
- */
- public abstract Object getKey();
-
- /**
- * Replacement key to try to load from cache instead of drawing the default image. This
- * is useful when we've already loaded a SIMPLE rendition, and are now loading the BEST
- * rendition. We want the BEST image to appear seamlessly on top of the existing SIMPLE
- * image.
- */
- public Object getKeyToShowInsteadOfDefault() {
- return null;
- }
- }
-
- /**
- * The thread that performs loading of photos from the database.
- */
- protected abstract class PhotoLoaderThread extends HandlerThread implements Callback {
-
- /**
- * Return photos mapped from {@link Request#getKey()} to the photo for
- * that request.
- */
- protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests);
-
- private static final int MESSAGE_LOAD_PHOTOS = 0;
-
- private final ContentResolver mResolver;
-
- private Handler mLoaderThreadHandler;
-
- public PhotoLoaderThread(ContentResolver resolver) {
- super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND);
- mResolver = resolver;
- }
-
- protected ContentResolver getResolver() {
- return mResolver;
- }
-
- public void ensureHandler() {
- if (mLoaderThreadHandler == null) {
- mLoaderThreadHandler = new Handler(getLooper(), this);
- }
- }
-
- /**
- * Sends a message to this thread to load requested photos. Cancels a preloading
- * request, if any: we don't want preloading to impede loading of the photos
- * we need to display now.
- */
- public void requestLoading() {
- ensureHandler();
- mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
- }
-
- /**
- * Receives the above message, loads photos and then sends a message
- * to the main thread to process them.
- */
- @Override
- public boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MESSAGE_LOAD_PHOTOS:
- loadPhotosInBackground();
- break;
- }
- return true;
- }
-
- /**
- * Subclasses may specify the maximum number of requests to be given at a time to
- * #loadPhotos(). For batch count N, the UI will be updated with up to N images at a time.
- *
- * @return A positive integer if you would like to limit the number of
- * items in a single batch.
- */
- protected int getMaxBatchCount() {
- return -1;
- }
-
- private void loadPhotosInBackground() {
- Utils.traceBeginSection("pre processing");
- final Collection<Request> loadRequests = new HashSet<PhotoManager.Request>();
- final Collection<Request> decodeRequests = new HashSet<PhotoManager.Request>();
- final PriorityQueue<Request> requests;
- synchronized (mPendingRequests) {
- requests = new PriorityQueue<Request>(mPendingRequests.values());
- }
-
- int batchCount = 0;
- int maxBatchCount = getMaxBatchCount();
- while (!requests.isEmpty()) {
- Request request = requests.poll();
- final BitmapHolder holder = sBitmapHolderCache
- .get(request.getKey());
- if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
- holder.width, holder.height, request.bitmapKey.w, request.bitmapKey.h)) {
- loadRequests.add(request);
- decodeRequests.add(request);
- batchCount++;
-
- final Message msg = Message.obtain();
- msg.what = MESSAGE_PHOTO_LOADING;
- msg.arg1 = request.hashCode();
- mMainThreadHandler.sendMessage(msg);
- } else {
- // Even if the image load is already done, this particular decode configuration
- // may not yet have run. Be sure to add it to the queue.
- if (sBitmapCache.get(request.bitmapKey) == null) {
- decodeRequests.add(request);
- }
- }
- request.attempts++;
- if (maxBatchCount > 0 && batchCount >= maxBatchCount) {
- break;
- }
- }
- Utils.traceEndSection();
-
- Utils.traceBeginSection("load photos");
- // Ask subclass to do the actual loading
- final Map<String, BitmapHolder> photosMap = loadPhotos(loadRequests);
- Utils.traceEndSection();
-
- if (DEBUG) {
- LogUtils.d(TAG,
- "worker thread completed read request batch. inputN=%s outputN=%s",
- loadRequests.size(),
- photosMap.size());
- }
- Utils.traceBeginSection("post processing");
- for (String cacheKey : photosMap.keySet()) {
- if (DEBUG) {
- LogUtils.d(TAG,
- "worker thread completed read request key=%s byteCount=%s thread=%s",
- cacheKey,
- photosMap.get(cacheKey) == null ? 0
- : photosMap.get(cacheKey).bytes.length,
- Thread.currentThread());
- }
- cacheBitmapHolder(cacheKey, photosMap.get(cacheKey));
- }
-
- for (Request r : decodeRequests) {
- if (sBitmapCache.get(r.bitmapKey) != null) {
- continue;
- }
-
- final Object cacheKey = r.getKey();
- final BitmapHolder holder = sBitmapHolderCache.get(cacheKey);
- if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
- holder.width, holder.height, r.bitmapKey.w, r.bitmapKey.h)) {
- continue;
- }
-
- final int w = r.bitmapKey.w;
- final int h = r.bitmapKey.h;
- final byte[] src = holder.bytes;
-
- if (w == 0 || h == 0) {
- LogUtils.e(TAG, new Error(), "bad dimensions for request=%s w/h=%s/%s",
- r, w, h);
- }
-
- final Bitmap decoded = BitmapUtil.decodeByteArrayWithCenterCrop(src, w, h);
- if (DEBUG) {
- LogUtils.i(TAG,
- "worker thread completed decode bmpKey=%s decoded=%s holder=%s",
- r.bitmapKey, decoded, holder);
- }
-
- if (decoded != null) {
- cacheBitmap(r.bitmapKey, decoded);
- }
- }
- Utils.traceEndSection();
-
- mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
- }
-
- protected String createInQuery(String value, int itemCount) {
- // Build first query
- StringBuilder query = new StringBuilder().append(value + " IN (");
- appendQuestionMarks(query, itemCount);
- query.append(')');
- return query.toString();
- }
-
- protected void appendQuestionMarks(StringBuilder query, int itemCount) {
- boolean first = true;
- for (int i = 0; i < itemCount; i++) {
- if (first) {
- first = false;
- } else {
- query.append(',');
- }
- query.append('?');
- }
- }
- }
-
- /**
- * An object to uniquely identify a combination of (Request + decoded size). Multiple requests
- * may require the same src image, but want to decode it into different sizes.
- */
- public static final class BitmapIdentifier {
- public final Object key;
- public final int w;
- public final int h;
-
- // OK to be static as long as all Requests are created on the same
- // thread
- private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions();
-
- public static BitmapIdentifier getBitmapKey(PhotoIdentifier id, ImageCanvas view,
- ImageCanvas.Dimensions dimensions) {
- final int width;
- final int height;
- if (dimensions != null) {
- width = dimensions.width;
- height = dimensions.height;
- } else {
- view.getDesiredDimensions(id.getKey(), sWorkDims);
- width = sWorkDims.width;
- height = sWorkDims.height;
- }
- return new BitmapIdentifier(id.getKey(), width, height);
- }
-
- public BitmapIdentifier(Object key, int w, int h) {
- this.key = key;
- this.w = w;
- this.h = h;
- }
-
- @Override
- public int hashCode() {
- int hash = 19;
- hash = 31 * hash + key.hashCode();
- hash = 31 * hash + w;
- hash = 31 * hash + h;
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null || obj.getClass() != getClass()) {
- return false;
- } else if (obj == this) {
- return true;
- }
- final BitmapIdentifier o = (BitmapIdentifier) obj;
- return Objects.equal(key, o.key) && w == o.w && h == o.h;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" key=");
- sb.append(key);
- sb.append(" w=");
- sb.append(w);
- sb.append(" h=");
- sb.append(h);
- sb.append("}");
- return sb.toString();
- }
- }
-
- /**
- * A holder for a contact photo request.
- */
- public final class Request implements Comparable<Request> {
- private final int mRequestedExtent;
- private final DefaultImageProvider mDefaultProvider;
- private final PhotoIdentifier mPhotoIdentifier;
- private final ImageCanvas mView;
- public final BitmapIdentifier bitmapKey;
- public final int viewGeneration;
- public int attempts;
-
- private Request(final PhotoIdentifier photoIdentifier,
- final DefaultImageProvider defaultProvider, final ImageCanvas view,
- final ImageCanvas.Dimensions dimensions) {
- mPhotoIdentifier = photoIdentifier;
- mRequestedExtent = -1;
- mDefaultProvider = defaultProvider;
- mView = view;
- viewGeneration = view.getGeneration();
-
- bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions);
- }
-
- public ImageCanvas getView() {
- return mView;
- }
-
- public PhotoIdentifier getPhotoIdentifier() {
- return mPhotoIdentifier;
- }
-
- /**
- * @see PhotoIdentifier#getKey()
- */
- public Object getKey() {
- return mPhotoIdentifier.getKey();
- }
-
- @Override
- public int hashCode() {
- return getHash(mPhotoIdentifier, mView);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null) return false;
- if (getClass() != obj.getClass()) return false;
- final Request that = (Request) obj;
- if (mRequestedExtent != that.mRequestedExtent) return false;
- if (!Objects.equal(mPhotoIdentifier, that.mPhotoIdentifier)) return false;
- if (!Objects.equal(mView, that.mView)) return false;
- // Don't compare equality of mDarkTheme because it is only used in the default contact
- // photo case. When the contact does have a photo, the contact photo is the same
- // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
- // twice.
- return true;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" key=");
- sb.append(getKey());
- sb.append(" id=");
- sb.append(mPhotoIdentifier);
- sb.append(" mView=");
- sb.append(mView);
- sb.append(" mExtent=");
- sb.append(mRequestedExtent);
- sb.append(" bitmapKey=");
- sb.append(bitmapKey);
- sb.append(" viewGeneration=");
- sb.append(viewGeneration);
- sb.append("}");
- return sb.toString();
- }
-
- public void applyDefaultImage() {
- if (mView.getGeneration() != viewGeneration) {
- // This can legitimately happen when an ImageCanvas is reused and re-purposed to
- // house a new set of images (e.g. by ListView recycling).
- // Ignore this now-stale request.
- if (DEBUG) {
- LogUtils.d(TAG,
- "ImageCanvas skipping applyDefaultImage; no longer contains" +
- " item=%s canvas=%s", getKey(), mView);
- }
- }
- mDefaultProvider.applyDefaultImage(mPhotoIdentifier, mView, mRequestedExtent);
- }
-
- @Override
- public int compareTo(Request another) {
- // Hold off on loading Requests which have failed before so it don't hold up others
- if (attempts - another.attempts != 0) {
- return attempts - another.attempts;
- }
- return mPhotoIdentifier.compareTo(another.mPhotoIdentifier);
- }
- }
-}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index c83e6e8..1b6000b 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -23,6 +23,7 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
import com.android.mail.widget.BaseWidgetProvider;
import com.google.common.collect.ImmutableSet;
@@ -40,7 +41,7 @@
*/
public final class MailPrefs extends VersionedPrefs {
- public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
+ public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
private static final String PREFS_NAME = "UnifiedEmail";
@@ -392,6 +393,11 @@
}
public boolean getShowSenderImages() {
+ if (Utils.isLowRamDevice(getContext())) {
+ // Do not show sender images in conversation list on low memory devices since they are
+ // expensive to render.
+ return false;
+ }
final SharedPreferences sharedPreferences = getSharedPreferences();
return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true);
}
diff --git a/src/com/android/mail/ui/AbstractMailActivity.java b/src/com/android/mail/ui/AbstractMailActivity.java
index 33a7da2..43f3b23 100644
--- a/src/com/android/mail/ui/AbstractMailActivity.java
+++ b/src/com/android/mail/ui/AbstractMailActivity.java
@@ -39,7 +39,7 @@
private final UiHandler mUiHandler = new UiHandler();
- private static final boolean STRICT_MODE = false;
+ private static final boolean STRICT_MODE = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fad7b3a..d47ea9c 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -40,6 +40,7 @@
import com.android.bitmap.DecodeAggregator;
import com.android.mail.R;
import com.android.mail.analytics.Analytics;
+import com.android.mail.bitmap.ContactResolver;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.ConversationItemViewCoordinates.CoordinatesCache;
@@ -93,17 +94,6 @@
private final Handler mHandler;
protected long mLastLeaveBehind = -1;
- private final BitmapCache mBitmapCache;
- private final DecodeAggregator mDecodeAggregator;
-
- public interface ConversationListListener {
- /**
- * @return <code>true</code> if the list is just exiting selection mode (so animations may
- * be required), <code>false</code> otherwise
- */
- boolean isExitingSelectionMode();
- }
-
private final AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
@Override
@@ -184,7 +174,6 @@
/** True if priority inbox markers are enabled, false otherwise. */
private boolean mPriorityMarkersEnabled;
private final ControllableActivity mActivity;
- private final ConversationListListener mConversationListListener;
private final AccountObserver mAccountListener = new AccountObserver() {
@Override
public void onChanged(Account newAccount) {
@@ -250,31 +239,43 @@
private static final String LOG_TAG = LogTag.getLogTag();
private static final int INCREASE_WAIT_COUNT = 2;
- private static final int BITMAP_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+ private final BitmapCache mAttachmentPreviewsCache;
+ private final DecodeAggregator mAttachmentPreviewsDecodeAggregator;
+ private final BitmapCache mSendersImagesCache;
+ private final ContactResolver mContactResolver;
+
+ private static final int ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+ /** 339KB cache fits 10 bitmaps at 33856 bytes each. */
+ private static final int SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES = 1024 * 339;
/**
* This is the fractional portion of the total cache size above that's dedicated to non-pooled
* bitmaps. (This is basically the portion of cache dedicated to GIFs.)
*/
- private static final float BITMAP_CACHE_NON_POOLED_FRACTION = 0.1f;
+ private static final float ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0.1f;
+ private static final float SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f;
+ /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */
+ private static final int SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100;
public AnimatedAdapter(Context context, ConversationCursor cursor,
ConversationSelectionSet batch, ControllableActivity activity,
- final ConversationListListener conversationListListener, SwipeableListView listView,
- final List<ConversationSpecialItemView> specialViews,
+ SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
final ObjectCursor<Folder> childFolders) {
super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
mContext = context;
mBatchConversations = batch;
setAccount(mAccountListener.initialize(activity.getAccountController()));
mActivity = activity;
- mConversationListListener = conversationListListener;
mShowFooter = false;
mListView = listView;
mFolderViews = getNestedFolders(childFolders);
- mBitmapCache = new AltBitmapCache(BITMAP_CACHE_TARGET_SIZE_BYTES,
- BITMAP_CACHE_NON_POOLED_FRACTION);
- mDecodeAggregator = new DecodeAggregator();
+ mAttachmentPreviewsCache = new AltBitmapCache(ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES,
+ ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION, 0);
+ mAttachmentPreviewsDecodeAggregator = new DecodeAggregator();
+ mSendersImagesCache = new AltBitmapCache(SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES,
+ SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION,
+ SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY);
+ mContactResolver = new ContactResolver(mContext.getContentResolver(), mSendersImagesCache);
mHandler = new Handler();
if (sDismissAllShortDelay == -1) {
@@ -415,10 +416,10 @@
if (view == null) {
view = new SwipeableConversationItemView(context, mAccount.name);
}
- view.bind(conv, mActivity, mConversationListListener, mBatchConversations, mFolder,
- getCheckboxSetting(), getAttachmentPreviewsSetting(),
- getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
- mSwipeEnabled, mPriorityMarkersEnabled, this);
+ view.bind(conv, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+ getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+ getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+ this);
return view;
}
@@ -803,10 +804,10 @@
SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView(
position, null, parent);
view.reset();
- view.bind(conversation, mActivity, mConversationListListener, mBatchConversations, mFolder,
- getCheckboxSetting(), getAttachmentPreviewsSetting(),
- getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
- mSwipeEnabled, mPriorityMarkersEnabled, this);
+ view.bind(conversation, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+ getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+ getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+ this);
mAnimatingViews.put(conversation.id, view);
return view;
}
@@ -1117,12 +1118,20 @@
return oldCursor;
}
- public BitmapCache getBitmapCache() {
- return mBitmapCache;
+ public BitmapCache getAttachmentPreviewsCache() {
+ return mAttachmentPreviewsCache;
}
- public DecodeAggregator getDecodeAggregator() {
- return mDecodeAggregator;
+ public DecodeAggregator getAttachmentPreviewsDecodeAggregator() {
+ return mAttachmentPreviewsDecodeAggregator;
+ }
+
+ public BitmapCache getSendersImagesCache() {
+ return mSendersImagesCache;
+ }
+
+ public ContactResolver getContactResolver() {
+ return mContactResolver;
}
/**
@@ -1178,7 +1187,7 @@
public void onScrollStateChanged(final int scrollState) {
final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
- mBitmapCache.setBlocking(scrolling);
+ mAttachmentPreviewsCache.setBlocking(scrolling);
}
public int getViewMode() {
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 32b8b3d..20a7c9f 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -60,7 +60,6 @@
import com.android.mail.providers.UIProvider.FolderCapabilities;
import com.android.mail.providers.UIProvider.FolderType;
import com.android.mail.providers.UIProvider.Swipe;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
import com.android.mail.ui.ViewMode.ModeChangeListener;
@@ -358,8 +357,7 @@
}
mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
- mActivity.getSelectedSet(), mActivity, mConversationListListener, mListView,
- specialItemViews, null);
+ mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
mSelectedSet = mActivity.getSelectedSet();
@@ -743,15 +741,6 @@
mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
}
- private final ConversationListListener mConversationListListener =
- new ConversationListListener() {
- @Override
- public boolean isExitingSelectionMode() {
- return System.currentTimeMillis() <
- (mSelectionModeExitedTimestamp + sSelectionModeAnimationDuration);
- }
- };
-
/**
* Sets the selected conversation to the position given here.
* @param cursorPosition The position of the conversation in the cursor (as opposed to
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 627a375..2c0ca14 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -419,7 +419,6 @@
if (adapter != null) {
adapter.onScrollStateChanged(scrollState);
}
- ConversationItemView.setScrollStateChanged(scrollState);
}
}
diff --git a/src/com/android/mail/photomanager/BitmapUtil.java b/src/com/android/mail/utils/BitmapUtil.java
similarity index 93%
rename from src/com/android/mail/photomanager/BitmapUtil.java
rename to src/com/android/mail/utils/BitmapUtil.java
index 9c2ab2b..6f61f56 100644
--- a/src/com/android/mail/photomanager/BitmapUtil.java
+++ b/src/com/android/mail/utils/BitmapUtil.java
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package com.android.mail.photomanager;
+package com.android.mail.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
-import com.android.mail.utils.LogUtils;
-
/**
* Provides static functions to decode bitmaps at the optimal size
*/
public class BitmapUtil {
+ private static final String TAG = LogTag.getLogTag();
private static final boolean DEBUG = false;
private BitmapUtil() {
@@ -53,7 +52,7 @@
opts.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(src, 0, src.length, opts);
} catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
+ LogUtils.w(TAG, t, "BitmapUtils unable to decode image");
return null;
}
}
@@ -73,7 +72,7 @@
return centerCrop(decoded, w, h);
} catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
+ LogUtils.w(TAG, t, "BitmapUtils unable to crop image");
return null;
}
}
@@ -161,13 +160,13 @@
final Bitmap cropped = Bitmap.createBitmap(src, srcX, srcY, srcCroppedW, srcCroppedH, m,
true /* filter */);
- if (DEBUG) LogUtils.i(PhotoManager.TAG,
- "IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
+ if (DEBUG) LogUtils.i(TAG,
+ "BitmapUtils IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
" innerW/H=%s/%s scale=%s resultW/H=%s/%s",
srcWidth, srcHeight, w, h, srcX, srcY, srcCroppedW, srcCroppedH, scale,
cropped.getWidth(), cropped.getHeight());
if (DEBUG && (w != cropped.getWidth() || h != cropped.getHeight())) {
- LogUtils.e(PhotoManager.TAG, new Error(), "last center crop violated assumptions.");
+ LogUtils.e(TAG, new Error(), "BitmapUtils last center crop violated assumptions.");
}
return cropped;
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index 5d4f434..3203422 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -64,7 +64,7 @@
* production releases. This should be set to DEBUG for production releases, and VERBOSE for
* internal builds.
*/
- private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+ private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
private static Boolean sDebugLoggingEnabledForTests = null;
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index de21172..c4927cd 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -22,6 +22,7 @@
import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
import com.google.common.collect.Maps;
+import android.app.ActivityManager;
import android.app.Fragment;
import android.app.SearchManager;
import android.content.Context;
@@ -146,6 +147,21 @@
}
/**
+ * @return Whether we are running on a low memory device. This is used to disable certain
+ * memory intensive features in the app.
+ */
+ public static boolean isLowRamDevice(Context context) {
+ // TODO: use SDK_INT to check if device is KitKat or greater.
+ if (Build.VERSION.CODENAME.startsWith("K")) {
+ final ActivityManager am = (ActivityManager) context.getSystemService(
+ Context.ACTIVITY_SERVICE);
+ return am.isLowRamDevice();
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Sets WebView in a restricted mode suitable for email use.
*
* @param webView The WebView to restrict