summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/media
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/datamodel/media')
-rw-r--r--src/com/android/messaging/datamodel/media/AsyncMediaRequestWrapper.java74
-rw-r--r--src/com/android/messaging/datamodel/media/AvatarGroupRequestDescriptor.java159
-rw-r--r--src/com/android/messaging/datamodel/media/AvatarRequest.java189
-rw-r--r--src/com/android/messaging/datamodel/media/AvatarRequestDescriptor.java60
-rw-r--r--src/com/android/messaging/datamodel/media/BindableMediaRequest.java63
-rw-r--r--src/com/android/messaging/datamodel/media/BugleMediaCacheManager.java54
-rw-r--r--src/com/android/messaging/datamodel/media/CompositeImageRequest.java109
-rw-r--r--src/com/android/messaging/datamodel/media/CompositeImageRequestDescriptor.java61
-rw-r--r--src/com/android/messaging/datamodel/media/CustomVCardEntry.java48
-rw-r--r--src/com/android/messaging/datamodel/media/CustomVCardEntryConstructor.java133
-rw-r--r--src/com/android/messaging/datamodel/media/DecodedImageResource.java254
-rw-r--r--src/com/android/messaging/datamodel/media/EncodedImageResource.java162
-rw-r--r--src/com/android/messaging/datamodel/media/FileImageRequest.java108
-rw-r--r--src/com/android/messaging/datamodel/media/FileImageRequestDescriptor.java68
-rw-r--r--src/com/android/messaging/datamodel/media/GifImageResource.java110
-rw-r--r--src/com/android/messaging/datamodel/media/ImageRequest.java258
-rw-r--r--src/com/android/messaging/datamodel/media/ImageRequestDescriptor.java113
-rw-r--r--src/com/android/messaging/datamodel/media/ImageResource.java63
-rw-r--r--src/com/android/messaging/datamodel/media/MediaBytes.java42
-rw-r--r--src/com/android/messaging/datamodel/media/MediaCache.java113
-rw-r--r--src/com/android/messaging/datamodel/media/MediaCacheManager.java69
-rw-r--r--src/com/android/messaging/datamodel/media/MediaRequest.java70
-rw-r--r--src/com/android/messaging/datamodel/media/MediaRequestDescriptor.java38
-rw-r--r--src/com/android/messaging/datamodel/media/MediaResourceManager.java325
-rw-r--r--src/com/android/messaging/datamodel/media/MessagePartImageRequestDescriptor.java64
-rw-r--r--src/com/android/messaging/datamodel/media/MessagePartVideoThumbnailRequestDescriptor.java38
-rw-r--r--src/com/android/messaging/datamodel/media/NetworkUriImageRequest.java120
-rw-r--r--src/com/android/messaging/datamodel/media/PoolableImageCache.java419
-rw-r--r--src/com/android/messaging/datamodel/media/RefCountedMediaResource.java164
-rw-r--r--src/com/android/messaging/datamodel/media/SimSelectorAvatarRequest.java117
-rw-r--r--src/com/android/messaging/datamodel/media/UriImageRequest.java58
-rw-r--r--src/com/android/messaging/datamodel/media/UriImageRequestDescriptor.java95
-rw-r--r--src/com/android/messaging/datamodel/media/VCardRequest.java328
-rw-r--r--src/com/android/messaging/datamodel/media/VCardRequestDescriptor.java35
-rw-r--r--src/com/android/messaging/datamodel/media/VCardResource.java52
-rw-r--r--src/com/android/messaging/datamodel/media/VCardResourceEntry.java389
-rw-r--r--src/com/android/messaging/datamodel/media/VideoThumbnailRequest.java78
-rw-r--r--src/com/android/messaging/datamodel/media/VideoThumbnailRequestDescriptor.java44
38 files changed, 4744 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/media/AsyncMediaRequestWrapper.java b/src/com/android/messaging/datamodel/media/AsyncMediaRequestWrapper.java
new file mode 100644
index 0000000..380d93c
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/AsyncMediaRequestWrapper.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+
+import java.util.List;
+
+/**
+ * A mix-in style class that wraps around a normal, threading-agnostic MediaRequest object with
+ * functionalities offered by {@link BindableMediaRequest} to allow for async processing.
+ */
+class AsyncMediaRequestWrapper<T extends RefCountedMediaResource> extends BindableMediaRequest<T> {
+
+ /**
+ * Create a new async media request wrapper instance given the listener.
+ */
+ public static <T extends RefCountedMediaResource> AsyncMediaRequestWrapper<T>
+ createWith(final MediaRequest<T> wrappedRequest,
+ final MediaResourceLoadListener<T> listener) {
+ return new AsyncMediaRequestWrapper<T>(listener, wrappedRequest);
+ }
+
+ private final MediaRequest<T> mWrappedRequest;
+
+ private AsyncMediaRequestWrapper(final MediaResourceLoadListener<T> listener,
+ final MediaRequest<T> wrappedRequest) {
+ super(listener);
+ mWrappedRequest = wrappedRequest;
+ }
+
+ @Override
+ public String getKey() {
+ return mWrappedRequest.getKey();
+ }
+
+ @Override
+ public MediaCache<T> getMediaCache() {
+ return mWrappedRequest.getMediaCache();
+ }
+
+ @Override
+ public int getRequestType() {
+ return mWrappedRequest.getRequestType();
+ }
+
+ @Override
+ public T loadMediaBlocking(List<MediaRequest<T>> chainedTask) throws Exception {
+ return mWrappedRequest.loadMediaBlocking(chainedTask);
+ }
+
+ @Override
+ public int getCacheId() {
+ return mWrappedRequest.getCacheId();
+ }
+
+ @Override
+ public MediaRequestDescriptor<T> getDescriptor() {
+ return mWrappedRequest.getDescriptor();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/AvatarGroupRequestDescriptor.java b/src/com/android/messaging/datamodel/media/AvatarGroupRequestDescriptor.java
new file mode 100644
index 0000000..719b296
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/AvatarGroupRequestDescriptor.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.net.Uri;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class AvatarGroupRequestDescriptor extends CompositeImageRequestDescriptor {
+ private static final int MAX_GROUP_SIZE = 4;
+
+ public AvatarGroupRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight) {
+ this(convertToDescriptor(uri, desiredWidth, desiredHeight), desiredWidth, desiredHeight);
+ }
+
+ public AvatarGroupRequestDescriptor(final List<? extends ImageRequestDescriptor> descriptors,
+ final int desiredWidth, final int desiredHeight) {
+ super(descriptors, desiredWidth, desiredHeight);
+ Assert.isTrue(descriptors.size() <= MAX_GROUP_SIZE);
+ }
+
+ private static List<? extends ImageRequestDescriptor> convertToDescriptor(final Uri uri,
+ final int desiredWidth, final int desiredHeight) {
+ final List<String> participantUriStrings = AvatarUriUtil.getGroupParticipantUris(uri);
+ final List<AvatarRequestDescriptor> avatarDescriptors =
+ new ArrayList<AvatarRequestDescriptor>(participantUriStrings.size());
+ for (final String uriString : participantUriStrings) {
+ final AvatarRequestDescriptor descriptor = new AvatarRequestDescriptor(
+ Uri.parse(uriString), desiredWidth, desiredHeight);
+ avatarDescriptors.add(descriptor);
+ }
+ return avatarDescriptors;
+ }
+
+ @Override
+ public CompositeImageRequest<?> buildBatchImageRequest(final Context context) {
+ return new CompositeImageRequest<AvatarGroupRequestDescriptor>(context, this);
+ }
+
+ @Override
+ public List<RectF> getChildRequestTargetRects() {
+ return Arrays.asList(generateDestRectArray());
+ }
+
+ /**
+ * Generates an array of {@link RectF} which represents where each of the individual avatar
+ * should be located in the final group avatar image. The location of each avatar depends on
+ * the size of the group and the size of the overall group avatar size.
+ */
+ private RectF[] generateDestRectArray() {
+ final int groupSize = mDescriptors.size();
+ final float width = desiredWidth;
+ final float height = desiredHeight;
+ final float halfWidth = width / 2F;
+ final float halfHeight = height / 2F;
+ final RectF[] destArray = new RectF[groupSize];
+ switch (groupSize) {
+ case 2:
+ /**
+ * +-------+
+ * | 0 | |
+ * +-------+
+ * | | 1 |
+ * +-------+
+ *
+ * We want two circles which touches in the center. To get this we know that the
+ * diagonal of the overall group avatar is squareRoot(2) * w We also know that the
+ * two circles touches the at the center of the overall group avatar and the
+ * distance from the center of the circle to the corner of the group avatar is
+ * radius * squareRoot(2). Therefore, the following emerges.
+ *
+ * w * squareRoot(2) = 2 (radius + radius * squareRoot(2))
+ * Solving for radius we get:
+ * d = 2 * radius = ( squareRoot(2) / (squareRoot(2) + 1)) * w
+ * d = (2 - squareRoot(2)) * w
+ */
+ final float diameter = (float) ((2 - Math.sqrt(2)) * width);
+ destArray[0] = new RectF(0, 0, diameter, diameter);
+ destArray[1] = new RectF(width - diameter, height - diameter, width, height);
+ break;
+ case 3:
+ /**
+ * +-------+
+ * | | 0 | |
+ * +-------+
+ * | 1 | 2 |
+ * +-------+
+ * i0
+ * |\
+ * a | \ c
+ * --- i2
+ * b
+ *
+ * a = radius * squareRoot(3) due to the triangle being a 30-60-90 right triangle.
+ * b = radius of circle
+ * c = 2 * radius of circle
+ *
+ * All three of the images are circles and therefore image zero will not touch
+ * image one or image two. Move image zero down so it touches image one and image
+ * two. This can be done by keeping image zero in the center and moving it down
+ * slightly. The amount to move down can be calculated by solving a right triangle.
+ * We know that the center x of image two to the center x of image zero is the
+ * radius of the circle, this is the length of edge b. Also we know that the
+ * distance from image zero to image two's center is 2 * radius, edge c. From this
+ * we know that the distance from center y of image two to center y of image one,
+ * edge a, is equal to radius * squareRoot(3) due to this triangle being a 30-60-90
+ * right triangle.
+ */
+ final float quarterWidth = width / 4F;
+ final float threeQuarterWidth = 3 * quarterWidth;
+ final float radius = height / 4F;
+ final float imageTwoCenterY = height - radius;
+ final float lengthOfEdgeA = (float) (radius * Math.sqrt(3));
+ final float imageZeroCenterY = imageTwoCenterY - lengthOfEdgeA;
+ final float imageZeroTop = imageZeroCenterY - radius;
+ final float imageZeroBottom = imageZeroCenterY + radius;
+ destArray[0] = new RectF(
+ quarterWidth, imageZeroTop, threeQuarterWidth, imageZeroBottom);
+ destArray[1] = new RectF(0, halfHeight, halfWidth, height);
+ destArray[2] = new RectF(halfWidth, halfHeight, width, height);
+ break;
+ default:
+ /**
+ * +-------+
+ * | 0 | 1 |
+ * +-------+
+ * | 2 | 3 |
+ * +-------+
+ */
+ destArray[0] = new RectF(0, 0, halfWidth, halfHeight);
+ destArray[1] = new RectF(halfWidth, 0, width, halfHeight);
+ destArray[2] = new RectF(0, halfHeight, halfWidth, height);
+ destArray[3] = new RectF(halfWidth, halfHeight, width, height);
+ break;
+ }
+ return destArray;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/AvatarRequest.java b/src/com/android/messaging/datamodel/media/AvatarRequest.java
new file mode 100644
index 0000000..22d5ccc
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/AvatarRequest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.ExifInterface;
+import android.net.Uri;
+
+import com.android.messaging.R;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.UriUtil;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public class AvatarRequest extends UriImageRequest<AvatarRequestDescriptor> {
+ private static Bitmap sDefaultPersonBitmap;
+ private static Bitmap sDefaultPersonBitmapLarge;
+
+ public AvatarRequest(final Context context,
+ final AvatarRequestDescriptor descriptor) {
+ super(context, descriptor);
+ }
+
+ @Override
+ protected InputStream getInputStreamForResource() throws FileNotFoundException {
+ if (UriUtil.isLocalResourceUri(mDescriptor.uri)) {
+ return super.getInputStreamForResource();
+ } else {
+ final Uri primaryUri = AvatarUriUtil.getPrimaryUri(mDescriptor.uri);
+ Assert.isTrue(UriUtil.isLocalResourceUri(primaryUri));
+ return mContext.getContentResolver().openInputStream(primaryUri);
+ }
+ }
+
+ /**
+ * We can load multiple types of images for avatars depending on the uri. The uri should be
+ * built by {@link com.android.messaging.util.AvatarUriUtil} which will decide on
+ * what uri to build based on the available profile photo and name. Here we will check if the
+ * image is a local resource (ie profile photo uri), if the resource isn't a local one we will
+ * generate a tile with the first letter of the name.
+ */
+ @Override
+ protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTasks)
+ throws IOException {
+ Assert.isNotMainThread();
+ String avatarType = AvatarUriUtil.getAvatarType(mDescriptor.uri);
+ Bitmap bitmap = null;
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ final boolean isLocalResourceUri = UriUtil.isLocalResourceUri(mDescriptor.uri) ||
+ AvatarUriUtil.TYPE_LOCAL_RESOURCE_URI.equals(avatarType);
+ if (isLocalResourceUri) {
+ try {
+ ImageResource imageResource = super.loadMediaInternal(chainedTasks);
+ bitmap = imageResource.getBitmap();
+ orientation = imageResource.mOrientation;
+ } catch (Exception ex) {
+ // If we encountered any exceptions trying to load the local avatar resource,
+ // fall back to generated avatar.
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "AvatarRequest: failed to load local avatar " +
+ "resource, switching to fallback rendering", ex);
+ }
+ }
+
+ final int width = mDescriptor.desiredWidth;
+ final int height = mDescriptor.desiredHeight;
+ // Check to see if we already got the bitmap. If not get a fallback avatar
+ if (bitmap == null) {
+ Uri generatedUri = mDescriptor.uri;
+ if (isLocalResourceUri) {
+ // If we are here, we just failed to load the local resource. Use the fallback Uri
+ // if possible.
+ generatedUri = AvatarUriUtil.getFallbackUri(mDescriptor.uri);
+ if (generatedUri == null) {
+ // No fallback Uri was provided, use the default avatar.
+ generatedUri = AvatarUriUtil.DEFAULT_BACKGROUND_AVATAR;
+ }
+ }
+
+ avatarType = AvatarUriUtil.getAvatarType(generatedUri);
+ if (AvatarUriUtil.TYPE_LETTER_TILE_URI.equals(avatarType)) {
+ final String name = AvatarUriUtil.getName(generatedUri);
+ bitmap = renderLetterTile(name, width, height);
+ } else {
+ bitmap = renderDefaultAvatar(width, height);
+ }
+ }
+ return new DecodedImageResource(getKey(), bitmap, orientation);
+ }
+
+ private Bitmap renderDefaultAvatar(final int width, final int height) {
+ final Bitmap bitmap = getBitmapPool().createOrReuseBitmap(width, height,
+ getBackgroundColor());
+ final Canvas canvas = new Canvas(bitmap);
+
+ if (sDefaultPersonBitmap == null) {
+ final BitmapDrawable defaultPerson = (BitmapDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ic_person_light);
+ sDefaultPersonBitmap = defaultPerson.getBitmap();
+ }
+ if (sDefaultPersonBitmapLarge == null) {
+ final BitmapDrawable largeDefaultPerson = (BitmapDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ic_person_light_large);
+ sDefaultPersonBitmapLarge = largeDefaultPerson.getBitmap();
+ }
+
+ Bitmap defaultPerson = null;
+ if (mDescriptor.isWearBackground) {
+ final BitmapDrawable wearDefaultPerson = (BitmapDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ic_person_wear);
+ defaultPerson = wearDefaultPerson.getBitmap();
+ } else {
+ final boolean isLargeDefault = (width > sDefaultPersonBitmap.getWidth()) ||
+ (height > sDefaultPersonBitmap.getHeight());
+ defaultPerson =
+ isLargeDefault ? sDefaultPersonBitmapLarge : sDefaultPersonBitmap;
+ }
+
+ final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ final Matrix matrix = new Matrix();
+ final RectF source = new RectF(0, 0, defaultPerson.getWidth(), defaultPerson.getHeight());
+ final RectF dest = new RectF(0, 0, width, height);
+ matrix.setRectToRect(source, dest, Matrix.ScaleToFit.FILL);
+
+ canvas.drawBitmap(defaultPerson, matrix, paint);
+
+ return bitmap;
+ }
+
+ private Bitmap renderLetterTile(final String name, final int width, final int height) {
+ final float halfWidth = width / 2;
+ final float halfHeight = height / 2;
+ final int minOfWidthAndHeight = Math.min(width, height);
+ final Bitmap bitmap = getBitmapPool().createOrReuseBitmap(width, height,
+ getBackgroundColor());
+ final Resources resources = mContext.getResources();
+ final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTypeface(Typeface.create("sans-serif-thin", Typeface.NORMAL));
+ paint.setColor(resources.getColor(R.color.letter_tile_font_color));
+ final float letterToTileRatio = resources.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
+ paint.setTextSize(letterToTileRatio * minOfWidthAndHeight);
+
+ final String firstCharString = name.substring(0, 1).toUpperCase();
+ final Rect textBound = new Rect();
+ paint.getTextBounds(firstCharString, 0, 1, textBound);
+
+ final Canvas canvas = new Canvas(bitmap);
+ final float xOffset = halfWidth - textBound.centerX();
+ final float yOffset = halfHeight - textBound.centerY();
+ canvas.drawText(firstCharString, xOffset, yOffset, paint);
+
+ return bitmap;
+ }
+
+ private int getBackgroundColor() {
+ return mContext.getResources().getColor(R.color.primary_color);
+ }
+
+ @Override
+ public int getCacheId() {
+ return BugleMediaCacheManager.AVATAR_IMAGE_CACHE;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/AvatarRequestDescriptor.java b/src/com/android/messaging/datamodel/media/AvatarRequestDescriptor.java
new file mode 100644
index 0000000..9afa9ad
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/AvatarRequestDescriptor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.UriUtil;
+
+public class AvatarRequestDescriptor extends UriImageRequestDescriptor {
+ final boolean isWearBackground;
+
+ public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight) {
+ this(uri, desiredWidth, desiredHeight, true /* cropToCircle */);
+ }
+
+ public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight, final boolean cropToCircle) {
+ this(uri, desiredWidth, desiredHeight, cropToCircle, false /* isWearBackground */);
+ }
+
+ public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight, boolean cropToCircle, boolean isWearBackground) {
+ super(uri, desiredWidth, desiredHeight, false /* allowCompression */, true /* isStatic */,
+ cropToCircle,
+ ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+ ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
+ Assert.isTrue(uri == null || UriUtil.isLocalResourceUri(uri) ||
+ AvatarUriUtil.isAvatarUri(uri));
+ this.isWearBackground = isWearBackground;
+ }
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
+ final String avatarType = uri == null ? null : AvatarUriUtil.getAvatarType(uri);
+ if (AvatarUriUtil.TYPE_SIM_SELECTOR_URI.equals(avatarType)) {
+ return new SimSelectorAvatarRequest(context, this);
+ } else {
+ return new AvatarRequest(context, this);
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/BindableMediaRequest.java b/src/com/android/messaging/datamodel/media/BindableMediaRequest.java
new file mode 100644
index 0000000..36521d5
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/BindableMediaRequest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import com.android.messaging.datamodel.binding.BindableOnceData;
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+
+/**
+ * The {@link MediaRequest} interface is threading-model-blind, allowing the implementations to
+ * be processed synchronously or asynchronously.
+ * This is a {@link MediaRequest} implementation that includes functionalities such as binding and
+ * event callbacks for multi-threaded media request processing.
+ */
+public abstract class BindableMediaRequest<T extends RefCountedMediaResource>
+ extends BindableOnceData
+ implements MediaRequest<T>, MediaResourceLoadListener<T> {
+ private MediaResourceLoadListener<T> mListener;
+
+ public BindableMediaRequest(final MediaResourceLoadListener<T> listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Delegates the media resource callback to the listener. Performs binding check to ensure
+ * the listener is still bound to this request.
+ */
+ @Override
+ public void onMediaResourceLoaded(final MediaRequest<T> request, final T resource,
+ final boolean cached) {
+ if (isBound() && mListener != null) {
+ mListener.onMediaResourceLoaded(request, resource, cached);
+ }
+ }
+
+ /**
+ * Delegates the media resource callback to the listener. Performs binding check to ensure
+ * the listener is still bound to this request.
+ */
+ @Override
+ public void onMediaResourceLoadError(final MediaRequest<T> request, final Exception exception) {
+ if (isBound() && mListener != null) {
+ mListener.onMediaResourceLoadError(request, exception);
+ }
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ mListener = null;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/BugleMediaCacheManager.java b/src/com/android/messaging/datamodel/media/BugleMediaCacheManager.java
new file mode 100644
index 0000000..c41ba60
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/BugleMediaCacheManager.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import com.android.messaging.util.Assert;
+
+/**
+ * An implementation of {@link MediaCacheManager} that creates caches specific to Bugle's needs.
+ *
+ * To create a new type of cache, add to the list of cache ids and create a new MediaCache<>
+ * for your cache id / media resource type in createMediaCacheById().
+ */
+public class BugleMediaCacheManager extends MediaCacheManager {
+ // List of available cache ids.
+ public static final int DEFAULT_IMAGE_CACHE = 1;
+ public static final int AVATAR_IMAGE_CACHE = 2;
+ public static final int VCARD_CACHE = 3;
+
+ // VCard cache size - we compute the size by count, not by bytes.
+ private static final int VCARD_CACHE_SIZE = 5;
+ private static final int SHARED_IMAGE_CACHE_SIZE = 1024 * 10; // 10MB
+
+ @Override
+ protected MediaCache<?> createMediaCacheById(final int id) {
+ switch (id) {
+ case DEFAULT_IMAGE_CACHE:
+ return new PoolableImageCache(SHARED_IMAGE_CACHE_SIZE, id, "DefaultImageCache");
+
+ case AVATAR_IMAGE_CACHE:
+ return new PoolableImageCache(id, "AvatarImageCache");
+
+ case VCARD_CACHE:
+ return new MediaCache<VCardResource>(VCARD_CACHE_SIZE, id, "VCardCache");
+
+ default:
+ Assert.fail("BugleMediaCacheManager: unsupported cache id " + id);
+ break;
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/CompositeImageRequest.java b/src/com/android/messaging/datamodel/media/CompositeImageRequest.java
new file mode 100644
index 0000000..66f1bff
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/CompositeImageRequest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.ImageUtils;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Requests a composite image resource. The composite image resource is constructed by first
+ * sequentially requesting a number of sub image resources specified by
+ * {@link CompositeImageRequestDescriptor#getChildRequestDescriptors()}. After this, the
+ * individual sub images are composed into the final image onto their respective target rects
+ * returned by {@link CompositeImageRequestDescriptor#getChildRequestTargetRects()}.
+ */
+public class CompositeImageRequest<D extends CompositeImageRequestDescriptor>
+ extends ImageRequest<D> {
+ private final Bitmap mBitmap;
+ private final Canvas mCanvas;
+ private final Paint mPaint;
+
+ public CompositeImageRequest(final Context context, final D descriptor) {
+ super(context, descriptor);
+ mBitmap = getBitmapPool().createOrReuseBitmap(
+ mDescriptor.desiredWidth, mDescriptor.desiredHeight);
+ mCanvas = new Canvas(mBitmap);
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ }
+
+ @Override
+ protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask) {
+ final List<? extends ImageRequestDescriptor> descriptors =
+ mDescriptor.getChildRequestDescriptors();
+ final List<RectF> targetRects = mDescriptor.getChildRequestTargetRects();
+ Assert.equals(descriptors.size(), targetRects.size());
+ Assert.isTrue(descriptors.size() > 1);
+
+ for (int i = 0; i < descriptors.size(); i++) {
+ final MediaRequest<ImageResource> request =
+ descriptors.get(i).buildSyncMediaRequest(mContext);
+ // Synchronously request the child image.
+ final ImageResource resource =
+ MediaResourceManager.get().requestMediaResourceSync(request);
+ if (resource != null) {
+ try {
+ final RectF avatarDestOnGroup = targetRects.get(i);
+
+ // Draw the bitmap into a smaller size with a circle mask.
+ final Bitmap resourceBitmap = resource.getBitmap();
+ final RectF resourceRect = new RectF(
+ 0, 0, resourceBitmap.getWidth(), resourceBitmap.getHeight());
+ final Bitmap smallCircleBitmap = getBitmapPool().createOrReuseBitmap(
+ Math.round(avatarDestOnGroup.width()),
+ Math.round(avatarDestOnGroup.height()));
+ final RectF smallCircleRect = new RectF(
+ 0, 0, smallCircleBitmap.getWidth(), smallCircleBitmap.getHeight());
+ final Canvas smallCircleCanvas = new Canvas(smallCircleBitmap);
+ ImageUtils.drawBitmapWithCircleOnCanvas(resource.getBitmap(), smallCircleCanvas,
+ resourceRect, smallCircleRect, null /* bitmapPaint */,
+ false /* fillBackground */,
+ ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+ ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
+ final Matrix matrix = new Matrix();
+ matrix.setRectToRect(smallCircleRect, avatarDestOnGroup,
+ Matrix.ScaleToFit.FILL);
+ mCanvas.drawBitmap(smallCircleBitmap, matrix, mPaint);
+ } finally {
+ resource.release();
+ }
+ }
+ }
+
+ return new DecodedImageResource(getKey(), mBitmap, ExifInterface.ORIENTATION_NORMAL);
+ }
+
+ @Override
+ public int getCacheId() {
+ return BugleMediaCacheManager.AVATAR_IMAGE_CACHE;
+ }
+
+ @Override
+ protected InputStream getInputStreamForResource() throws FileNotFoundException {
+ throw new IllegalStateException("Composite image request doesn't support input stream!");
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/CompositeImageRequestDescriptor.java b/src/com/android/messaging/datamodel/media/CompositeImageRequestDescriptor.java
new file mode 100644
index 0000000..071130e
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/CompositeImageRequestDescriptor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.RectF;
+
+import com.google.common.base.Joiner;
+
+import java.util.List;
+
+public abstract class CompositeImageRequestDescriptor extends ImageRequestDescriptor {
+ protected final List<? extends ImageRequestDescriptor> mDescriptors;
+ private final String mKey;
+
+ public CompositeImageRequestDescriptor(final List<? extends ImageRequestDescriptor> descriptors,
+ final int desiredWidth, final int desiredHeight) {
+ super(desiredWidth, desiredHeight);
+ mDescriptors = descriptors;
+
+ final String[] keyParts = new String[descriptors.size()];
+ for (int i = 0; i < descriptors.size(); i++) {
+ keyParts[i] = descriptors.get(i).getKey();
+ }
+ mKey = Joiner.on(",").skipNulls().join(keyParts);
+ }
+
+ /**
+ * Gets a key that uniquely identify all the underlying image resource to be loaded (e.g. Uri or
+ * file path).
+ */
+ @Override
+ public String getKey() {
+ return mKey;
+ }
+
+ public List<? extends ImageRequestDescriptor> getChildRequestDescriptors(){
+ return mDescriptors;
+ }
+
+ public abstract List<RectF> getChildRequestTargetRects();
+ public abstract CompositeImageRequest<?> buildBatchImageRequest(final Context context);
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
+ return buildBatchImageRequest(context);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/CustomVCardEntry.java b/src/com/android/messaging/datamodel/media/CustomVCardEntry.java
new file mode 100644
index 0000000..aee9fdc
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/CustomVCardEntry.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.accounts.Account;
+import android.support.v4.util.ArrayMap;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardProperty;
+
+import java.util.Map;
+
+/**
+ * Class which extends VCardEntry to add support for unknown properties. Currently there is a TODO
+ * to add this in the VCardEntry code, but we have to extend it to add the needed support
+ */
+public class CustomVCardEntry extends VCardEntry {
+ // List of properties keyed by their name for easy lookup
+ private final Map<String, VCardProperty> mAllProperties;
+
+ public CustomVCardEntry(int vCardType, Account account) {
+ super(vCardType, account);
+ mAllProperties = new ArrayMap<String, VCardProperty>();
+ }
+
+ @Override
+ public void addProperty(VCardProperty property) {
+ super.addProperty(property);
+ mAllProperties.put(property.getName(), property);
+ }
+
+ public VCardProperty getProperty(String name) {
+ return mAllProperties.get(name);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/CustomVCardEntryConstructor.java b/src/com/android/messaging/datamodel/media/CustomVCardEntryConstructor.java
new file mode 100644
index 0000000..06b10a3
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/CustomVCardEntryConstructor.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.accounts.Account;
+import com.android.vcard.VCardConfig;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomVCardEntryConstructor implements VCardInterpreter {
+
+ public interface EntryHandler {
+ /**
+ * Called when the parsing started.
+ */
+ public void onStart();
+
+ /**
+ * The method called when one vCard entry is created. Children come before their parent in
+ * nested vCard files.
+ *
+ * e.g.
+ * In the following vCard, the entry for "entry2" comes before one for "entry1".
+ * <code>
+ * BEGIN:VCARD
+ * N:entry1
+ * BEGIN:VCARD
+ * N:entry2
+ * END:VCARD
+ * END:VCARD
+ * </code>
+ */
+ public void onEntryCreated(final CustomVCardEntry entry);
+
+ /**
+ * Called when the parsing ended.
+ * Able to be use this method for showing performance log, etc.
+ */
+ public void onEnd();
+ }
+
+ /**
+ * Represents current stack of VCardEntry. Used to support nested vCard (vCard 2.1).
+ */
+ private final List<CustomVCardEntry> mEntryStack = new ArrayList<CustomVCardEntry>();
+ private CustomVCardEntry mCurrentEntry;
+
+ private final int mVCardType;
+ private final Account mAccount;
+
+ private final List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>();
+
+ public CustomVCardEntryConstructor() {
+ this(VCardConfig.VCARD_TYPE_V21_GENERIC, null);
+ }
+
+ public CustomVCardEntryConstructor(final int vcardType) {
+ this(vcardType, null);
+ }
+
+ public CustomVCardEntryConstructor(final int vcardType, final Account account) {
+ mVCardType = vcardType;
+ mAccount = account;
+ }
+
+ public void addEntryHandler(EntryHandler entryHandler) {
+ mEntryHandlers.add(entryHandler);
+ }
+
+ @Override
+ public void onVCardStarted() {
+ for (EntryHandler entryHandler : mEntryHandlers) {
+ entryHandler.onStart();
+ }
+ }
+
+ @Override
+ public void onVCardEnded() {
+ for (EntryHandler entryHandler : mEntryHandlers) {
+ entryHandler.onEnd();
+ }
+ }
+
+ public void clear() {
+ mCurrentEntry = null;
+ mEntryStack.clear();
+ }
+
+ @Override
+ public void onEntryStarted() {
+ mCurrentEntry = new CustomVCardEntry(mVCardType, mAccount);
+ mEntryStack.add(mCurrentEntry);
+ }
+
+ @Override
+ public void onEntryEnded() {
+ mCurrentEntry.consolidateFields();
+ for (EntryHandler entryHandler : mEntryHandlers) {
+ entryHandler.onEntryCreated(mCurrentEntry);
+ }
+
+ final int size = mEntryStack.size();
+ if (size > 1) {
+ CustomVCardEntry parent = mEntryStack.get(size - 2);
+ parent.addChild(mCurrentEntry);
+ mCurrentEntry = parent;
+ } else {
+ mCurrentEntry = null;
+ }
+ mEntryStack.remove(size - 1);
+ }
+
+ @Override
+ public void onPropertyCreated(VCardProperty property) {
+ mCurrentEntry.addProperty(property);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/DecodedImageResource.java b/src/com/android/messaging/datamodel/media/DecodedImageResource.java
new file mode 100644
index 0000000..3627ba4
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/DecodedImageResource.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+import com.android.messaging.ui.OrientedBitmapDrawable;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.OsUtil;
+
+import java.util.List;
+
+
+/**
+ * Container class for holding a bitmap resource used by the MediaResourceManager. This resource
+ * can both be cached (albeit not very storage-efficiently) and directly used by the UI.
+ */
+public class DecodedImageResource extends ImageResource {
+ private static final int BITMAP_QUALITY = 100;
+ private static final int COMPRESS_QUALITY = 50;
+
+ private Bitmap mBitmap;
+ private final int mOrientation;
+ private boolean mCacheable = true;
+
+ public DecodedImageResource(final String key, final Bitmap bitmap, int orientation) {
+ super(key, orientation);
+ mBitmap = bitmap;
+ mOrientation = orientation;
+ }
+
+ /**
+ * Gets the contained bitmap.
+ */
+ @Override
+ public Bitmap getBitmap() {
+ acquireLock();
+ try {
+ return mBitmap;
+ } finally {
+ releaseLock();
+ }
+ }
+
+ /**
+ * Attempt to reuse the bitmap in the image resource and repurpose it for something else.
+ * After this, the image resource will relinquish ownership on the bitmap resource so that
+ * it doesn't try to recycle it when getting closed.
+ */
+ @Override
+ public Bitmap reuseBitmap() {
+ acquireLock();
+ try {
+ assertSingularRefCount();
+ final Bitmap retBitmap = mBitmap;
+ mBitmap = null;
+ return retBitmap;
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ public boolean supportsBitmapReuse() {
+ return true;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ acquireLock();
+ try {
+ return ImageUtils.bitmapToBytes(mBitmap, BITMAP_QUALITY);
+ } catch (final Exception e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "Error trying to get the bitmap bytes " + e);
+ } finally {
+ releaseLock();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the orientation of the image as one of the ExifInterface.ORIENTATION_* constants
+ */
+ @Override
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ @Override
+ public int getMediaSize() {
+ acquireLock();
+ try {
+ Assert.notNull(mBitmap);
+ if (OsUtil.isAtLeastKLP()) {
+ return mBitmap.getAllocationByteCount();
+ } else {
+ return mBitmap.getRowBytes() * mBitmap.getHeight();
+ }
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ protected void close() {
+ acquireLock();
+ try {
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ public Drawable getDrawable(Resources resources) {
+ acquireLock();
+ try {
+ Assert.notNull(mBitmap);
+ return OrientedBitmapDrawable.create(getOrientation(), resources, mBitmap);
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ boolean isCacheable() {
+ return mCacheable;
+ }
+
+ public void setCacheable(final boolean cacheable) {
+ mCacheable = cacheable;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ MediaRequest<? extends RefCountedMediaResource> getMediaEncodingRequest(
+ final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
+ Assert.isFalse(isEncoded());
+ if (getBitmap().hasAlpha()) {
+ // We can't compress images with alpha, as JPEG encoding doesn't support this.
+ return null;
+ }
+ return new EncodeImageRequest((MediaRequest<ImageResource>) originalRequest);
+ }
+
+ /**
+ * A MediaRequest that encodes the contained image resource.
+ */
+ private class EncodeImageRequest implements MediaRequest<ImageResource> {
+ private final MediaRequest<ImageResource> mOriginalImageRequest;
+
+ public EncodeImageRequest(MediaRequest<ImageResource> originalImageRequest) {
+ mOriginalImageRequest = originalImageRequest;
+ // Hold a ref onto the encoded resource before the request finishes.
+ DecodedImageResource.this.addRef();
+ }
+
+ @Override
+ public String getKey() {
+ return DecodedImageResource.this.getKey();
+ }
+
+ @Override
+ @DoesNotRunOnMainThread
+ public ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedRequests)
+ throws Exception {
+ Assert.isNotMainThread();
+ acquireLock();
+ Bitmap scaledBitmap = null;
+ try {
+ Bitmap bitmap = getBitmap();
+ Assert.isFalse(bitmap.hasAlpha());
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+ // The original bitmap was loaded using sub-sampling which was fast in terms of
+ // loading speed, but not optimized for caching, encoding and rendering (since
+ // bitmap resizing to fit the UI image views happens on the UI thread and should
+ // be avoided if possible). Therefore, try to resize the bitmap to the exact desired
+ // size before compressing it.
+ if (bitmapWidth > 0 && bitmapHeight > 0 &&
+ mOriginalImageRequest instanceof ImageRequest<?>) {
+ final ImageRequestDescriptor descriptor =
+ ((ImageRequest<?>) mOriginalImageRequest).getDescriptor();
+ final float targetScale = Math.max(
+ (float) descriptor.desiredWidth / bitmapWidth,
+ (float) descriptor.desiredHeight / bitmapHeight);
+ final int targetWidth = (int) (bitmapWidth * targetScale);
+ final int targetHeight = (int) (bitmapHeight * targetScale);
+ // Only try to scale down the image to the desired size.
+ if (targetScale < 1.0f && targetWidth > 0 && targetHeight > 0 &&
+ targetWidth != bitmapWidth && targetHeight != bitmapHeight) {
+ scaledBitmap = bitmap =
+ Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false);
+ }
+ }
+ byte[] encodedBytes = ImageUtils.bitmapToBytes(bitmap, COMPRESS_QUALITY);
+ return new EncodedImageResource(getKey(), encodedBytes, getOrientation());
+ } catch (Exception ex) {
+ // Something went wrong during bitmap compression, fall back to just using the
+ // original bitmap.
+ LogUtil.e(LogUtil.BUGLE_IMAGE_TAG, "Error compressing bitmap", ex);
+ return DecodedImageResource.this;
+ } finally {
+ if (scaledBitmap != null && scaledBitmap != getBitmap()) {
+ scaledBitmap.recycle();
+ scaledBitmap = null;
+ }
+ releaseLock();
+ release();
+ }
+ }
+
+ @Override
+ public MediaCache<ImageResource> getMediaCache() {
+ return mOriginalImageRequest.getMediaCache();
+ }
+
+ @Override
+ public int getCacheId() {
+ return mOriginalImageRequest.getCacheId();
+ }
+
+ @Override
+ public int getRequestType() {
+ return REQUEST_ENCODE_MEDIA;
+ }
+
+ @Override
+ public MediaRequestDescriptor<ImageResource> getDescriptor() {
+ return mOriginalImageRequest.getDescriptor();
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/EncodedImageResource.java b/src/com/android/messaging/datamodel/media/EncodedImageResource.java
new file mode 100644
index 0000000..0bc94e5
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/EncodedImageResource.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A cache-facing image resource that's much more compact than the raw Bitmap objects stored in
+ * {@link com.android.messaging.datamodel.media.DecodedImageResource}.
+ *
+ * This resource is created from a regular Bitmap-based ImageResource before being pushed to
+ * {@link com.android.messaging.datamodel.media.MediaCache}, if the image request
+ * allows for resource encoding/compression.
+ *
+ * During resource retrieval on cache hit,
+ * {@link #getMediaDecodingRequest(MediaRequest)} is invoked to create a async
+ * decode task, which decodes the compressed byte array back to a regular image resource to
+ * be consumed by the UI.
+ */
+public class EncodedImageResource extends ImageResource {
+ private final byte[] mImageBytes;
+
+ public EncodedImageResource(String key, byte[] imageBytes, int orientation) {
+ super(key, orientation);
+ mImageBytes = imageBytes;
+ }
+
+ @Override
+ @DoesNotRunOnMainThread
+ public Bitmap getBitmap() {
+ acquireLock();
+ try {
+ // This should only be called during the decode request.
+ Assert.isNotMainThread();
+ return BitmapFactory.decodeByteArray(mImageBytes, 0, mImageBytes.length);
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ public byte[] getBytes() {
+ acquireLock();
+ try {
+ return Arrays.copyOf(mImageBytes, mImageBytes.length);
+ } finally {
+ releaseLock();
+ }
+ }
+
+ @Override
+ public Bitmap reuseBitmap() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsBitmapReuse() {
+ return false;
+ }
+
+ @Override
+ public int getMediaSize() {
+ return mImageBytes.length;
+ }
+
+ @Override
+ protected void close() {
+ }
+
+ @Override
+ public Drawable getDrawable(Resources resources) {
+ return null;
+ }
+
+ @Override
+ boolean isEncoded() {
+ return true;
+ }
+
+ @Override
+ MediaRequest<? extends RefCountedMediaResource> getMediaDecodingRequest(
+ final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
+ Assert.isTrue(isEncoded());
+ return new DecodeImageRequest();
+ }
+
+ /**
+ * A MediaRequest that decodes the encoded image resource. This class is chained to the
+ * original media request that requested the image, so it inherits the listener and
+ * properties such as binding.
+ */
+ private class DecodeImageRequest implements MediaRequest<ImageResource> {
+ public DecodeImageRequest() {
+ // Hold a ref onto the encoded resource before the request finishes.
+ addRef();
+ }
+
+ @Override
+ public String getKey() {
+ return EncodedImageResource.this.getKey();
+ }
+
+ @Override
+ @DoesNotRunOnMainThread
+ public ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedTask)
+ throws Exception {
+ Assert.isNotMainThread();
+ acquireLock();
+ try {
+ final Bitmap decodedBitmap = BitmapFactory.decodeByteArray(mImageBytes, 0,
+ mImageBytes.length);
+ return new DecodedImageResource(getKey(), decodedBitmap, getOrientation());
+ } finally {
+ releaseLock();
+ release();
+ }
+ }
+
+ @Override
+ public MediaCache<ImageResource> getMediaCache() {
+ // Decoded resource is non-cachable, it's for UI consumption only (for now at least)
+ return null;
+ }
+
+ @Override
+ public int getCacheId() {
+ return 0;
+ }
+
+ @Override
+ public int getRequestType() {
+ return REQUEST_DECODE_MEDIA;
+ }
+
+ @Override
+ public MediaRequestDescriptor<ImageResource> getDescriptor() {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/FileImageRequest.java b/src/com/android/messaging/datamodel/media/FileImageRequest.java
new file mode 100644
index 0000000..31c053a
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/FileImageRequest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+
+import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool;
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.LogUtil;
+
+import java.io.IOException;
+
+/**
+ * Serves file system based image requests. Since file paths can be expressed in Uri form, this
+ * extends regular UriImageRequest but performs additional optimizations such as loading thumbnails
+ * directly from Exif information.
+ */
+public class FileImageRequest extends UriImageRequest {
+ private final String mPath;
+ private final boolean mCanUseThumbnail;
+
+ public FileImageRequest(final Context context,
+ final FileImageRequestDescriptor descriptor) {
+ super(context, descriptor);
+ mPath = descriptor.path;
+ mCanUseThumbnail = descriptor.canUseThumbnail;
+ }
+
+ @Override
+ protected Bitmap loadBitmapInternal()
+ throws IOException {
+ // Before using the FileInputStream, check if the Exif has a thumbnail that we can use.
+ if (mCanUseThumbnail) {
+ byte[] thumbnail = null;
+ try {
+ final ExifInterface exif = new ExifInterface(mPath);
+ if (exif.hasThumbnail()) {
+ thumbnail = exif.getThumbnail();
+ }
+ } catch (final IOException e) {
+ // Nothing to do
+ }
+
+ if (thumbnail != null) {
+ final BitmapFactory.Options options = PoolableImageCache.getBitmapOptionsForPool(
+ false /* scaled */, 0 /* inputDensity */, 0 /* targetDensity */);
+ // First, check dimensions of the bitmap.
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = ImageUtils.get().calculateInSampleSize(options,
+ mDescriptor.desiredWidth, mDescriptor.desiredHeight);
+
+ options.inJustDecodeBounds = false;
+
+ // Actually decode the bitmap, optionally using the bitmap pool.
+ try {
+ // Get the orientation. We should be able to get the orientation from
+ // the thumbnail itself but at least on some phones, the thumbnail
+ // doesn't have an orientation tag. So use the outer image's orientation
+ // tag and hope for the best.
+ mOrientation = ImageUtils.getOrientation(getInputStreamForResource());
+ if (com.android.messaging.util.exif.ExifInterface.
+ getOrientationParams(mOrientation).invertDimensions) {
+ mDescriptor.updateSourceDimensions(options.outHeight, options.outWidth);
+ } else {
+ mDescriptor.updateSourceDimensions(options.outWidth, options.outHeight);
+ }
+ final ReusableImageResourcePool bitmapPool = getBitmapPool();
+ if (bitmapPool == null) {
+ return BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length,
+ options);
+ } else {
+ final int sampledWidth = options.outWidth / options.inSampleSize;
+ final int sampledHeight = options.outHeight / options.inSampleSize;
+ return bitmapPool.decodeByteArray(thumbnail, options, sampledWidth,
+ sampledHeight);
+ }
+ } catch (IOException ex) {
+ // If the thumbnail is broken due to IOException, this will
+ // fall back to default bitmap loading.
+ LogUtil.e(LogUtil.BUGLE_IMAGE_TAG, "FileImageRequest: failed to load " +
+ "thumbnail from Exif", ex);
+ }
+ }
+ }
+
+ // Fall back to default InputStream-based loading if no thumbnails could be retrieved.
+ return super.loadBitmapInternal();
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/FileImageRequestDescriptor.java b/src/com/android/messaging/datamodel/media/FileImageRequestDescriptor.java
new file mode 100644
index 0000000..00105f5
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/FileImageRequestDescriptor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.UriUtil;
+
+/**
+ * Holds image request info about file system based image resource.
+ */
+public class FileImageRequestDescriptor extends UriImageRequestDescriptor {
+ public final String path;
+
+ // Can we use the thumbnail image from Exif data?
+ public final boolean canUseThumbnail;
+
+ /**
+ * Convenience constructor for when the image file's dimensions are not known.
+ */
+ public FileImageRequestDescriptor(final String path, final int desiredWidth,
+ final int desiredHeight, final boolean canUseThumbnail, final boolean canCompress,
+ final boolean isStatic) {
+ this(path, desiredWidth, desiredHeight, FileImageRequest.UNSPECIFIED_SIZE,
+ FileImageRequest.UNSPECIFIED_SIZE, canUseThumbnail, canCompress, isStatic);
+ }
+
+ /**
+ * Creates a new file image request with this descriptor. Oftentimes image file metadata
+ * has information such as the size of the image. Provide these metrics if they are known.
+ */
+ public FileImageRequestDescriptor(final String path, final int desiredWidth,
+ final int desiredHeight, final int sourceWidth, final int sourceHeight,
+ final boolean canUseThumbnail, final boolean canCompress, final boolean isStatic) {
+ super(UriUtil.getUriForResourceFile(path), desiredWidth, desiredHeight, sourceWidth,
+ sourceHeight, canCompress, isStatic, false /* cropToCircle */,
+ ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+ ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
+ this.path = path;
+ this.canUseThumbnail = canUseThumbnail;
+ }
+
+ @Override
+ public String getKey() {
+ final String prefixKey = super.getKey();
+ return prefixKey == null ? null : new StringBuilder(prefixKey).append(KEY_PART_DELIMITER)
+ .append(canUseThumbnail).toString();
+ }
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
+ return new FileImageRequest(context, this);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/GifImageResource.java b/src/com/android/messaging/datamodel/media/GifImageResource.java
new file mode 100644
index 0000000..d50cf47
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/GifImageResource.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.media.ExifInterface;
+import android.support.rastermill.FrameSequence;
+import android.support.rastermill.FrameSequenceDrawable;
+
+import com.android.messaging.util.Assert;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class GifImageResource extends ImageResource {
+ private FrameSequence mFrameSequence;
+
+ public GifImageResource(String key, FrameSequence frameSequence) {
+ // GIF does not support exif tags
+ super(key, ExifInterface.ORIENTATION_NORMAL);
+ mFrameSequence = frameSequence;
+ }
+
+ public static GifImageResource createGifImageResource(String key, InputStream inputStream) {
+ final FrameSequence frameSequence;
+ try {
+ frameSequence = FrameSequence.decodeStream(inputStream);
+ } finally {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ // Nothing to do if we fail closing the stream
+ }
+ }
+ if (frameSequence == null) {
+ return null;
+ }
+ return new GifImageResource(key, frameSequence);
+ }
+
+ @Override
+ public Drawable getDrawable(Resources resources) {
+ return new FrameSequenceDrawable(mFrameSequence);
+ }
+
+ @Override
+ public Bitmap getBitmap() {
+ Assert.fail("GetBitmap() should never be called on a gif.");
+ return null;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ Assert.fail("GetBytes() should never be called on a gif.");
+ return null;
+ }
+
+ @Override
+ public Bitmap reuseBitmap() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsBitmapReuse() {
+ // FrameSequenceDrawable a.) takes two bitmaps and thus does not fit into the current
+ // bitmap pool architecture b.) will rarely use bitmaps from one FrameSequenceDrawable to
+ // the next that are the same sizes since they are used by attachments.
+ return false;
+ }
+
+ @Override
+ public int getMediaSize() {
+ Assert.fail("GifImageResource should not be used by a media cache");
+ // Only used by the media cache, which this does not use.
+ return 0;
+ }
+
+ @Override
+ public boolean isCacheable() {
+ return false;
+ }
+
+ @Override
+ protected void close() {
+ acquireLock();
+ try {
+ if (mFrameSequence != null) {
+ mFrameSequence = null;
+ }
+ } finally {
+ releaseLock();
+ }
+ }
+
+}
diff --git a/src/com/android/messaging/datamodel/media/ImageRequest.java b/src/com/android/messaging/datamodel/media/ImageRequest.java
new file mode 100644
index 0000000..ab8880d
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/ImageRequest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.exif.ExifInterface;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Base class that serves an image request for resolving, retrieving and decoding bitmap resources.
+ *
+ * Subclasses may choose to load images from different medium, such as from the file system or
+ * from the local content resolver, by overriding the abstract getInputStreamForResource() method.
+ */
+public abstract class ImageRequest<D extends ImageRequestDescriptor>
+ implements MediaRequest<ImageResource> {
+ public static final int UNSPECIFIED_SIZE = MessagePartData.UNSPECIFIED_SIZE;
+
+ protected final Context mContext;
+ protected final D mDescriptor;
+ protected int mOrientation;
+
+ /**
+ * Creates a new image request with the given descriptor.
+ */
+ public ImageRequest(final Context context, final D descriptor) {
+ mContext = context;
+ mDescriptor = descriptor;
+ }
+
+ /**
+ * Gets a key that uniquely identify the underlying image resource to be loaded (e.g. Uri or
+ * file path).
+ */
+ @Override
+ public String getKey() {
+ return mDescriptor.getKey();
+ }
+
+ /**
+ * Returns the image request descriptor attached to this request.
+ */
+ @Override
+ public D getDescriptor() {
+ return mDescriptor;
+ }
+
+ @Override
+ public int getRequestType() {
+ return MediaRequest.REQUEST_LOAD_MEDIA;
+ }
+
+ /**
+ * Allows sub classes to specify that they want us to call getBitmapForResource rather than
+ * getInputStreamForResource
+ */
+ protected boolean hasBitmapObject() {
+ return false;
+ }
+
+ protected Bitmap getBitmapForResource() throws IOException {
+ return null;
+ }
+
+ /**
+ * Retrieves an input stream from which image resource could be loaded.
+ * @throws FileNotFoundException
+ */
+ protected abstract InputStream getInputStreamForResource() throws FileNotFoundException;
+
+ /**
+ * Loads the image resource. This method is final; to override the media loading behavior
+ * the subclass should override {@link #loadMediaInternal(List)}
+ */
+ @Override
+ public final ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedTask)
+ throws IOException {
+ Assert.isNotMainThread();
+ final ImageResource loadedResource = loadMediaInternal(chainedTask);
+ return postProcessOnBitmapResourceLoaded(loadedResource);
+ }
+
+ protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask)
+ throws IOException {
+ if (!mDescriptor.isStatic() && isGif()) {
+ final GifImageResource gifImageResource =
+ GifImageResource.createGifImageResource(getKey(), getInputStreamForResource());
+ if (gifImageResource == null) {
+ throw new RuntimeException("Error decoding gif");
+ }
+ return gifImageResource;
+ } else {
+ final Bitmap loadedBitmap = loadBitmapInternal();
+ if (loadedBitmap == null) {
+ throw new RuntimeException("failed decoding bitmap");
+ }
+ return new DecodedImageResource(getKey(), loadedBitmap, mOrientation);
+ }
+ }
+
+ protected boolean isGif() throws FileNotFoundException {
+ return ImageUtils.isGif(getInputStreamForResource());
+ }
+
+ /**
+ * The internal routine for loading the image. The caller may optionally provide the width
+ * and height of the source image if known so that we don't need to manually decode those.
+ */
+ protected Bitmap loadBitmapInternal() throws IOException {
+
+ final boolean unknownSize = mDescriptor.sourceWidth == UNSPECIFIED_SIZE ||
+ mDescriptor.sourceHeight == UNSPECIFIED_SIZE;
+
+ // If the ImageRequest has a Bitmap object rather than a stream, there's little to do here
+ if (hasBitmapObject()) {
+ final Bitmap bitmap = getBitmapForResource();
+ if (bitmap != null && unknownSize) {
+ mDescriptor.updateSourceDimensions(bitmap.getWidth(), bitmap.getHeight());
+ }
+ return bitmap;
+ }
+
+ mOrientation = ImageUtils.getOrientation(getInputStreamForResource());
+
+ final BitmapFactory.Options options = PoolableImageCache.getBitmapOptionsForPool(
+ false /* scaled */, 0 /* inputDensity */, 0 /* targetDensity */);
+ // First, check dimensions of the bitmap if not already known.
+ if (unknownSize) {
+ final InputStream inputStream = getInputStreamForResource();
+ if (inputStream != null) {
+ try {
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(inputStream, null, options);
+ // This is called when dimensions of image were unknown to allow db update
+ if (ExifInterface.getOrientationParams(mOrientation).invertDimensions) {
+ mDescriptor.updateSourceDimensions(options.outHeight, options.outWidth);
+ } else {
+ mDescriptor.updateSourceDimensions(options.outWidth, options.outHeight);
+ }
+ } finally {
+ inputStream.close();
+ }
+ } else {
+ throw new FileNotFoundException();
+ }
+ } else {
+ options.outWidth = mDescriptor.sourceWidth;
+ options.outHeight = mDescriptor.sourceHeight;
+ }
+
+ // Calculate inSampleSize
+ options.inSampleSize = ImageUtils.get().calculateInSampleSize(options,
+ mDescriptor.desiredWidth, mDescriptor.desiredHeight);
+ Assert.isTrue(options.inSampleSize > 0);
+
+ // Reopen the input stream and actually decode the bitmap. The initial
+ // BitmapFactory.decodeStream() reads the header portion of the bitmap stream and leave
+ // the input stream at the last read position. Since this input stream doesn't support
+ // mark() and reset(), the only viable way to reload the input stream is to re-open it.
+ // Alternatively, we could decode the bitmap into a byte array first and act on the byte
+ // array, but that also means the entire bitmap (for example a 10MB image from the gallery)
+ // without downsampling will have to be loaded into memory up front, which we don't want
+ // as it gives a much bigger possibility of OOM when handling big images. Therefore, the
+ // solution here is to close and reopen the bitmap input stream.
+ // For inline images the size is cached in DB and this hit is only taken once per image
+ final InputStream inputStream = getInputStreamForResource();
+ if (inputStream != null) {
+ try {
+ options.inJustDecodeBounds = false;
+
+ // Actually decode the bitmap, optionally using the bitmap pool.
+ final ReusableImageResourcePool bitmapPool = getBitmapPool();
+ if (bitmapPool == null) {
+ return BitmapFactory.decodeStream(inputStream, null, options);
+ } else {
+ final int sampledWidth = (options.outWidth + options.inSampleSize - 1) /
+ options.inSampleSize;
+ final int sampledHeight = (options.outHeight + options.inSampleSize - 1) /
+ options.inSampleSize;
+ return bitmapPool.decodeSampledBitmapFromInputStream(
+ inputStream, options, sampledWidth, sampledHeight);
+ }
+ } finally {
+ inputStream.close();
+ }
+ } else {
+ throw new FileNotFoundException();
+ }
+ }
+
+ private ImageResource postProcessOnBitmapResourceLoaded(final ImageResource loadedResource) {
+ if (mDescriptor.cropToCircle && loadedResource instanceof DecodedImageResource) {
+ final int width = mDescriptor.desiredWidth;
+ final int height = mDescriptor.desiredHeight;
+ final Bitmap sourceBitmap = loadedResource.getBitmap();
+ final Bitmap targetBitmap = getBitmapPool().createOrReuseBitmap(width, height);
+ final RectF dest = new RectF(0, 0, width, height);
+ final RectF source = new RectF(0, 0, sourceBitmap.getWidth(), sourceBitmap.getHeight());
+ final int backgroundColor = mDescriptor.circleBackgroundColor;
+ final int strokeColor = mDescriptor.circleStrokeColor;
+ ImageUtils.drawBitmapWithCircleOnCanvas(sourceBitmap, new Canvas(targetBitmap), source,
+ dest, null, backgroundColor == 0 ? false : true /* fillBackground */,
+ backgroundColor, strokeColor);
+ return new DecodedImageResource(getKey(), targetBitmap,
+ loadedResource.getOrientation());
+ }
+ return loadedResource;
+ }
+
+ /**
+ * Returns the bitmap pool for this image request.
+ */
+ protected ReusableImageResourcePool getBitmapPool() {
+ return MediaCacheManager.get().getOrCreateBitmapPoolForCache(getCacheId());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public MediaCache<ImageResource> getMediaCache() {
+ return (MediaCache<ImageResource>) MediaCacheManager.get().getOrCreateMediaCacheById(
+ getCacheId());
+ }
+
+ /**
+ * Returns the cache id. Subclasses may override this to use a different cache.
+ */
+ @Override
+ public int getCacheId() {
+ return BugleMediaCacheManager.DEFAULT_IMAGE_CACHE;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/ImageRequestDescriptor.java b/src/com/android/messaging/datamodel/media/ImageRequestDescriptor.java
new file mode 100644
index 0000000..20cb9af
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/ImageRequestDescriptor.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+
+import com.android.messaging.util.Assert;
+
+/**
+ * The base ImageRequest descriptor that describes the requirement of the requested image
+ * resource, including the desired size. It holds request info that will be consumed by
+ * ImageRequest instances. Subclasses of ImageRequest are expected to take
+ * more descriptions such as content URI or file path.
+ */
+public abstract class ImageRequestDescriptor extends MediaRequestDescriptor<ImageResource> {
+ /** Desired size for the image (if known). This is used for bitmap downsampling */
+ public final int desiredWidth;
+ public final int desiredHeight;
+
+ /** Source size of the image (if known). This is used so that we don't have to manually decode
+ * the metrics from the image resource */
+ public final int sourceWidth;
+ public final int sourceHeight;
+
+ /**
+ * A static image resource is required, even if the image format supports animation (like Gif).
+ */
+ public final boolean isStatic;
+
+ /**
+ * The loaded image will be cropped to circular shape.
+ */
+ public final boolean cropToCircle;
+
+ /**
+ * The loaded image will be cropped to circular shape with the background color.
+ */
+ public final int circleBackgroundColor;
+
+ /**
+ * The loaded image will be cropped to circular shape with a stroke color.
+ */
+ public final int circleStrokeColor;
+
+ protected static final char KEY_PART_DELIMITER = '|';
+
+ /**
+ * Creates a new image request with unspecified width and height. In this case, the full
+ * bitmap is loaded and decoded, so unless you are sure that the image will be of
+ * reasonable size, you should consider limiting at least one of the two dimensions
+ * (for example, limiting the image width to the width of the ImageView container).
+ */
+ public ImageRequestDescriptor() {
+ this(ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE,
+ ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE, false, false, 0, 0);
+ }
+
+ public ImageRequestDescriptor(final int desiredWidth, final int desiredHeight) {
+ this(desiredWidth, desiredHeight,
+ ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE, false, false, 0, 0);
+ }
+
+ public ImageRequestDescriptor(final int desiredWidth,
+ final int desiredHeight, final int sourceWidth, final int sourceHeight,
+ final boolean isStatic, final boolean cropToCircle, final int circleBackgroundColor,
+ int circleStrokeColor) {
+ Assert.isTrue(desiredWidth == ImageRequest.UNSPECIFIED_SIZE || desiredWidth > 0);
+ Assert.isTrue(desiredHeight == ImageRequest.UNSPECIFIED_SIZE || desiredHeight > 0);
+ Assert.isTrue(sourceWidth == ImageRequest.UNSPECIFIED_SIZE || sourceWidth > 0);
+ Assert.isTrue(sourceHeight == ImageRequest.UNSPECIFIED_SIZE || sourceHeight > 0);
+ this.desiredWidth = desiredWidth;
+ this.desiredHeight = desiredHeight;
+ this.sourceWidth = sourceWidth;
+ this.sourceHeight = sourceHeight;
+ this.isStatic = isStatic;
+ this.cropToCircle = cropToCircle;
+ this.circleBackgroundColor = circleBackgroundColor;
+ this.circleStrokeColor = circleStrokeColor;
+ }
+
+ public String getKey() {
+ return new StringBuilder()
+ .append(desiredWidth).append(KEY_PART_DELIMITER)
+ .append(desiredHeight).append(KEY_PART_DELIMITER)
+ .append(String.valueOf(cropToCircle)).append(KEY_PART_DELIMITER)
+ .append(String.valueOf(circleBackgroundColor)).append(KEY_PART_DELIMITER)
+ .append(String.valueOf(isStatic)).toString();
+ }
+
+ public boolean isStatic() {
+ return isStatic;
+ }
+
+ @Override
+ public abstract MediaRequest<ImageResource> buildSyncMediaRequest(Context context);
+
+ // Called once source dimensions finally determined upon loading the image
+ public void updateSourceDimensions(final int sourceWidth, final int sourceHeight) {
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/ImageResource.java b/src/com/android/messaging/datamodel/media/ImageResource.java
new file mode 100644
index 0000000..75d817d
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/ImageResource.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Base class for holding some form of image resource. The subclass gets to define the specific
+ * type of data format it's holding, whether it be Bitmap objects or compressed byte arrays.
+ */
+public abstract class ImageResource extends RefCountedMediaResource {
+ protected final int mOrientation;
+
+ public ImageResource(final String key, int orientation) {
+ super(key);
+ mOrientation = orientation;
+ }
+
+ /**
+ * Gets the contained image in drawable format.
+ */
+ public abstract Drawable getDrawable(Resources resources);
+
+ /**
+ * Gets the contained image in bitmap format.
+ */
+ public abstract Bitmap getBitmap();
+
+ /**
+ * Gets the contained image in byte array format.
+ */
+ public abstract byte[] getBytes();
+
+ /**
+ * Attempt to reuse the bitmap in the image resource and re-purpose it for something else.
+ * After this, the image resource will relinquish ownership on the bitmap resource so that
+ * it doesn't try to recycle it when getting closed.
+ */
+ public abstract Bitmap reuseBitmap();
+ public abstract boolean supportsBitmapReuse();
+
+ /**
+ * Gets the orientation of the image as one of the ExifInterface.ORIENTATION_* constants
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/MediaBytes.java b/src/com/android/messaging/datamodel/media/MediaBytes.java
new file mode 100644
index 0000000..823bf27
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaBytes.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+
+/**
+ * Container class for handing around media information used by the MediaResourceManager.
+ */
+public class MediaBytes extends RefCountedMediaResource {
+ private final byte[] mBytes;
+
+ public MediaBytes(final String key, final byte[] bytes) {
+ super(key);
+ mBytes = bytes;
+ }
+
+ public byte[] getMediaBytes() {
+ return mBytes;
+ }
+
+ @Override
+ public int getMediaSize() {
+ return mBytes.length;
+ }
+
+ @Override
+ protected void close() {
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/MediaCache.java b/src/com/android/messaging/datamodel/media/MediaCache.java
new file mode 100644
index 0000000..510da2d
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaCache.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.util.LruCache;
+
+import com.android.messaging.util.LogUtil;
+
+/**
+ * A modified LruCache that is able to hold RefCountedMediaResource instances. It releases
+ * ref on the entries as they are evicted from the cache, and it uses the media resource
+ * size in kilobytes, instead of the entry count, as the size of the cache.
+ *
+ * This class is used by the MediaResourceManager class to maintain a number of caches for
+ * holding different types of {@link RefCountedMediaResource}
+ */
+public class MediaCache<T extends RefCountedMediaResource> extends LruCache<String, T> {
+ private static final String TAG = LogUtil.BUGLE_IMAGE_TAG;
+
+ // Default memory cache size in kilobytes
+ protected static final int DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES = 1024 * 5; // 5MB
+
+ // Unique identifier for the cache.
+ private final int mId;
+ // Descriptive name given to the cache for debugging purposes.
+ private final String mName;
+
+ // Convenience constructor that uses the default cache size.
+ public MediaCache(final int id, final String name) {
+ this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
+ }
+
+ public MediaCache(final int maxSize, final int id, final String name) {
+ super(maxSize);
+ mId = id;
+ mName = name;
+ }
+
+ public void destroy() {
+ evictAll();
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets a media resource from this cache. Must use this method to get resource instead of get()
+ * to ensure addRef() on the resource.
+ */
+ public synchronized T fetchResourceFromCache(final String key) {
+ final T ret = get(key);
+ if (ret != null) {
+ if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(TAG, "cache hit in mediaCache @ " + getName() +
+ ", total cache hit = " + hitCount() +
+ ", total cache miss = " + missCount());
+ }
+ ret.addRef();
+ } else if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(TAG, "cache miss in mediaCache @ " + getName() +
+ ", total cache hit = " + hitCount() +
+ ", total cache miss = " + missCount());
+ }
+ return ret;
+ }
+
+ /**
+ * Add a media resource to this cache. Must use this method to add resource instead of put()
+ * to ensure addRef() on the resource.
+ */
+ public synchronized T addResourceToCache(final String key, final T mediaResource) {
+ mediaResource.addRef();
+ return put(key, mediaResource);
+ }
+
+ /**
+ * Notify the removed entry that is no longer being cached
+ */
+ @Override
+ protected synchronized void entryRemoved(final boolean evicted, final String key,
+ final T oldValue, final T newValue) {
+ oldValue.release();
+ }
+
+ /**
+ * Measure item size in kilobytes rather than units which is more practical
+ * for a media resource cache
+ */
+ @Override
+ protected int sizeOf(final String key, final T value) {
+ final int mediaSizeInKilobytes = value.getMediaSize() / 1024;
+ // Never zero-count any resource, count as at least 1KB.
+ return mediaSizeInKilobytes == 0 ? 1 : mediaSizeInKilobytes;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/MediaCacheManager.java b/src/com/android/messaging/datamodel/media/MediaCacheManager.java
new file mode 100644
index 0000000..6e029f2
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaCacheManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.util.SparseArray;
+
+import com.android.messaging.Factory;
+import com.android.messaging.datamodel.MemoryCacheManager;
+import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
+import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool;
+
+/**
+ * Manages a set of media caches by id.
+ */
+public abstract class MediaCacheManager implements MemoryCache {
+ public static MediaCacheManager get() {
+ return Factory.get().getMediaCacheManager();
+ }
+
+ protected final SparseArray<MediaCache<?>> mCaches;
+
+ public MediaCacheManager() {
+ mCaches = new SparseArray<MediaCache<?>>();
+ MemoryCacheManager.get().registerMemoryCache(this);
+ }
+
+ @Override
+ public void reclaim() {
+ final int count = mCaches.size();
+ for (int i = 0; i < count; i++) {
+ mCaches.valueAt(i).destroy();
+ }
+ mCaches.clear();
+ }
+
+ public synchronized MediaCache<?> getOrCreateMediaCacheById(final int id) {
+ MediaCache<?> cache = mCaches.get(id);
+ if (cache == null) {
+ cache = createMediaCacheById(id);
+ if (cache != null) {
+ mCaches.put(id, cache);
+ }
+ }
+ return cache;
+ }
+
+ public ReusableImageResourcePool getOrCreateBitmapPoolForCache(final int cacheId) {
+ final MediaCache<?> cache = getOrCreateMediaCacheById(cacheId);
+ if (cache != null && cache instanceof PoolableImageCache) {
+ return ((PoolableImageCache) cache).asReusableBitmapPool();
+ }
+ return null;
+ }
+
+ protected abstract MediaCache<?> createMediaCacheById(final int id);
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/MediaRequest.java b/src/com/android/messaging/datamodel/media/MediaRequest.java
new file mode 100644
index 0000000..703671b
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaRequest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import java.util.List;
+
+/**
+ * Keeps track of a media loading request. MediaResourceManager uses this interface to load, encode,
+ * decode, and cache different types of media resource.
+ *
+ * This interface defines a media request class that's threading-model-blind. Wrapper classes
+ * (such as {@link AsyncMediaRequestWrapper} wraps around any base media request to offer async
+ * extensions).
+ */
+public interface MediaRequest<T extends RefCountedMediaResource> {
+ public static final int REQUEST_ENCODE_MEDIA = 1;
+ public static final int REQUEST_DECODE_MEDIA = 2;
+ public static final int REQUEST_LOAD_MEDIA = 3;
+
+ /**
+ * Returns a unique key used for storing and looking up the MediaRequest.
+ */
+ String getKey();
+
+ /**
+ * This method performs the heavy-lifting work of synchronously loading the media bytes for
+ * this MediaRequest on a single threaded executor.
+ * @param chainedTask subsequent tasks to be performed after this request is complete. For
+ * example, an image request may need to compress the image resource before putting it in the
+ * cache
+ */
+ T loadMediaBlocking(List<MediaRequest<T>> chainedTask) throws Exception;
+
+ /**
+ * Returns the media cache where this MediaRequest wants to store the loaded
+ * media resource.
+ */
+ MediaCache<T> getMediaCache();
+
+ /**
+ * Returns the id of the cache where this MediaRequest wants to store the loaded
+ * media resource.
+ */
+ int getCacheId();
+
+ /**
+ * Returns the request type of this media request, i.e. one of {@link #REQUEST_ENCODE_MEDIA},
+ * {@link #REQUEST_DECODE_MEDIA}, or {@link #REQUEST_LOAD_MEDIA}. The default is
+ * {@link #REQUEST_LOAD_MEDIA}
+ */
+ int getRequestType();
+
+ /**
+ * Returns the descriptor defining the request.
+ */
+ MediaRequestDescriptor<T> getDescriptor();
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/MediaRequestDescriptor.java b/src/com/android/messaging/datamodel/media/MediaRequestDescriptor.java
new file mode 100644
index 0000000..216b2a3
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaRequestDescriptor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+
+/**
+ * The base data holder/builder class for constructing async/sync MediaRequest objects during
+ * runtime.
+ */
+public abstract class MediaRequestDescriptor<T extends RefCountedMediaResource> {
+ public abstract MediaRequest<T> buildSyncMediaRequest(Context context);
+
+ /**
+ * Builds an async media request to be used with
+ * {@link MediaResourceManager#requestMediaResourceAsync(MediaRequest)}
+ */
+ public BindableMediaRequest<T> buildAsyncMediaRequest(final Context context,
+ final MediaResourceLoadListener<T> listener) {
+ final MediaRequest<T> syncRequest = buildSyncMediaRequest(context);
+ return AsyncMediaRequestWrapper.createWith(syncRequest, listener);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/MediaResourceManager.java b/src/com/android/messaging/datamodel/media/MediaResourceManager.java
new file mode 100644
index 0000000..13f7291
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MediaResourceManager.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.os.AsyncTask;
+
+import com.android.messaging.Factory;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.RunsOnAnyThread;
+import com.android.messaging.util.LogUtil;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * <p>Loads and maintains a set of in-memory LRU caches for different types of media resources.
+ * Right now we don't utilize any disk cache as all media urls are expected to be resolved to
+ * local content.<p/>
+ *
+ * <p>The MediaResourceManager takes media loading requests through one of two ways:</p>
+ *
+ * <ol>
+ * <li>{@link #requestMediaResourceAsync(MediaRequest)} that takes a MediaRequest, which may be a
+ * regular request if the caller doesn't want to listen for events (fire-and-forget),
+ * or an async request wrapper if event callback is needed.</li>
+ * <li>{@link #requestMediaResourceSync(MediaRequest)} which takes a MediaRequest and synchronously
+ * returns the loaded result, or null if failed.</li>
+ * </ol>
+ *
+ * <p>For each media loading task, MediaResourceManager starts an AsyncTask that runs on a
+ * dedicated thread, which calls MediaRequest.loadMediaBlocking() to perform the actual media
+ * loading work. As the media resources are loaded, MediaResourceManager notifies the callers
+ * (which must implement the MediaResourceLoadListener interface) via onMediaResourceLoaded()
+ * callback. Meanwhile, MediaResourceManager also pushes the loaded resource onto its dedicated
+ * cache.</p>
+ *
+ * <p>The media resource caches ({@link MediaCache}) are maintained as a set of LRU caches. They are
+ * created on demand by the incoming MediaRequest's getCacheId() method. The implementations of
+ * MediaRequest (such as {@link ImageRequest}) get to determine the desired cache id. For Bugle,
+ * the list of available caches are in {@link BugleMediaCacheManager}</p>
+ *
+ * <p>Optionally, media loading can support on-demand media encoding and decoding.
+ * All {@link MediaRequest}'s can opt to chain additional {@link MediaRequest}'s to be executed
+ * after the completion of the main media loading task, by adding new tasks to the chained
+ * task list in {@link MediaRequest#loadMediaBlocking(List)}. One possible type of chained task is
+ * media encoding task. Loaded media will be encoded on a dedicated single threaded executor
+ * *after* the UI is notified of the loaded media. In this case, the encoded media resource will
+ * be eventually pushed to the cache, which will later be decoded before posting to the UI thread
+ * on cache hit.</p>
+ *
+ * <p><b>To add support for a new type of media resource,</b></p>
+ *
+ * <ol>
+ * <li>Create a new subclass of {@link RefCountedMediaResource} for the new resource type (example:
+ * {@link ImageResource} class).</li>
+ *
+ * <li>Implement the {@link MediaRequest} interface (example: {@link ImageRequest}). Perform the
+ * media loading work in loadMediaBlocking() and return a cache id in getCacheId().</li>
+ *
+ * <li>For the UI component that requests the media resource, let it implement
+ * {@link MediaResourceLoadListener} interface to listen for resource load callback. Let the
+ * UI component call MediaResourceManager.requestMediaResourceAsync() to request a media source.
+ * (example: {@link com.android.messaging.ui.ContactIconView}</li>
+ * </ol>
+ */
+public class MediaResourceManager {
+ private static final String TAG = LogUtil.BUGLE_TAG;
+
+ public static MediaResourceManager get() {
+ return Factory.get().getMediaResourceManager();
+ }
+
+ /**
+ * Listener for asynchronous callback from media loading events.
+ */
+ public interface MediaResourceLoadListener<T extends RefCountedMediaResource> {
+ void onMediaResourceLoaded(MediaRequest<T> request, T resource, boolean cached);
+ void onMediaResourceLoadError(MediaRequest<T> request, Exception exception);
+ }
+
+ // We use a fixed thread pool for handling media loading tasks. Using a cached thread pool
+ // allows for unlimited thread creation which can lead to OOMs so we limit the threads here.
+ private static final Executor MEDIA_LOADING_EXECUTOR = Executors.newFixedThreadPool(10);
+
+ // A dedicated single thread executor for performing background task after loading the resource
+ // on the media loading executor. This includes work such as encoding loaded media to be cached.
+ // These tasks are run on a single worker thread with low priority so as not to contend with the
+ // media loading tasks.
+ private static final Executor MEDIA_BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor(
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final Thread encodingThread = new Thread(runnable);
+ encodingThread.setPriority(Thread.MIN_PRIORITY);
+ return encodingThread;
+ }
+ });
+
+ /**
+ * Requests a media resource asynchronously. Upon completion of the media loading task,
+ * the listener will be notified of success/failure iff it's still bound. A refcount on the
+ * resource is held and guaranteed for the caller for the duration of the
+ * {@link MediaResourceLoadListener#onMediaResourceLoaded(
+ * MediaRequest, RefCountedMediaResource, boolean)} callback.
+ * @param mediaRequest the media request. May be either an
+ * {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
+ * request for fire-and-forget type of behavior.
+ */
+ public <T extends RefCountedMediaResource> void requestMediaResourceAsync(
+ final MediaRequest<T> mediaRequest) {
+ scheduleAsyncMediaRequest(mediaRequest, MEDIA_LOADING_EXECUTOR);
+ }
+
+ /**
+ * Requests a media resource synchronously.
+ * @return the loaded resource with a refcount reserved for the caller. The caller must call
+ * release() on the resource once it's done using it (like with Cursors).
+ */
+ public <T extends RefCountedMediaResource> T requestMediaResourceSync(
+ final MediaRequest<T> mediaRequest) {
+ Assert.isNotMainThread();
+ // Block and load media.
+ MediaLoadingResult<T> loadResult = null;
+ try {
+ loadResult = processMediaRequestInternal(mediaRequest);
+ // The loaded resource should have at least one refcount by now reserved for the caller.
+ Assert.isTrue(loadResult.loadedResource.getRefCount() > 0);
+ return loadResult.loadedResource;
+ } catch (final Exception e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "Synchronous media loading failed, key=" +
+ mediaRequest.getKey(), e);
+ return null;
+ } finally {
+ if (loadResult != null) {
+ // Schedule the background requests chained to the main request.
+ loadResult.scheduleChainedRequests();
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends RefCountedMediaResource> MediaLoadingResult<T> processMediaRequestInternal(
+ final MediaRequest<T> mediaRequest)
+ throws Exception {
+ final List<MediaRequest<T>> chainedRequests = new ArrayList<>();
+ T loadedResource = null;
+ // Try fetching from cache first.
+ final T cachedResource = loadMediaFromCache(mediaRequest);
+ if (cachedResource != null) {
+ if (cachedResource.isEncoded()) {
+ // The resource is encoded, issue a decoding request.
+ final MediaRequest<T> decodeRequest = (MediaRequest<T>) cachedResource
+ .getMediaDecodingRequest(mediaRequest);
+ Assert.notNull(decodeRequest);
+ cachedResource.release();
+ loadedResource = loadMediaFromRequest(decodeRequest, chainedRequests);
+ } else {
+ // The resource is ready-to-use.
+ loadedResource = cachedResource;
+ }
+ } else {
+ // Actually load the media after cache miss.
+ loadedResource = loadMediaFromRequest(mediaRequest, chainedRequests);
+ }
+ return new MediaLoadingResult<>(loadedResource, cachedResource != null /* fromCache */,
+ chainedRequests);
+ }
+
+ private <T extends RefCountedMediaResource> T loadMediaFromCache(
+ final MediaRequest<T> mediaRequest) {
+ if (mediaRequest.getRequestType() != MediaRequest.REQUEST_LOAD_MEDIA) {
+ // Only look up in the cache if we are loading media.
+ return null;
+ }
+ final MediaCache<T> mediaCache = mediaRequest.getMediaCache();
+ if (mediaCache != null) {
+ final T mediaResource = mediaCache.fetchResourceFromCache(mediaRequest.getKey());
+ if (mediaResource != null) {
+ return mediaResource;
+ }
+ }
+ return null;
+ }
+
+ private <T extends RefCountedMediaResource> T loadMediaFromRequest(
+ final MediaRequest<T> mediaRequest, final List<MediaRequest<T>> chainedRequests)
+ throws Exception {
+ final T resource = mediaRequest.loadMediaBlocking(chainedRequests);
+ // mediaRequest.loadMediaBlocking() should never return null without
+ // throwing an exception.
+ Assert.notNull(resource);
+ // It's possible for the media to be evicted right after it's added to
+ // the cache (possibly because it's by itself too big for the cache).
+ // It's also possible that, after added to the cache, something else comes
+ // to the cache and evicts this media resource. To prevent this from
+ // recycling the underlying resource objects, make sure to add ref before
+ // adding to cache so that the caller is guaranteed a ref on the resource.
+ resource.addRef();
+ // Don't cache the media request if it is defined as non-cacheable.
+ if (resource.isCacheable()) {
+ addResourceToMemoryCache(mediaRequest, resource);
+ }
+ return resource;
+ }
+
+ /**
+ * Schedule an async media request on the given <code>executor</code>.
+ * @param mediaRequest the media request to be processed asynchronously. May be either an
+ * {@link AsyncMediaRequestWrapper} for listening for event callbacks, or a regular media
+ * request for fire-and-forget type of behavior.
+ */
+ private <T extends RefCountedMediaResource> void scheduleAsyncMediaRequest(
+ final MediaRequest<T> mediaRequest, final Executor executor) {
+ final BindableMediaRequest<T> bindableRequest =
+ (mediaRequest instanceof BindableMediaRequest<?>) ?
+ (BindableMediaRequest<T>) mediaRequest : null;
+ if (bindableRequest != null && !bindableRequest.isBound()) {
+ return; // Request is obsolete
+ }
+ // We don't use SafeAsyncTask here since it enforces the shared thread pool executor
+ // whereas we want a dedicated thread pool executor.
+ AsyncTask<Void, Void, MediaLoadingResult<T>> mediaLoadingTask =
+ new AsyncTask<Void, Void, MediaLoadingResult<T>>() {
+ private Exception mException;
+
+ @Override
+ protected MediaLoadingResult<T> doInBackground(Void... params) {
+ // Double check the request is still valid by the time we start processing it
+ if (bindableRequest != null && !bindableRequest.isBound()) {
+ return null; // Request is obsolete
+ }
+ try {
+ return processMediaRequestInternal(mediaRequest);
+ } catch (Exception e) {
+ mException = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final MediaLoadingResult<T> result) {
+ if (result != null) {
+ Assert.isNull(mException);
+ Assert.isTrue(result.loadedResource.getRefCount() > 0);
+ try {
+ if (bindableRequest != null) {
+ bindableRequest.onMediaResourceLoaded(
+ bindableRequest, result.loadedResource, result.fromCache);
+ }
+ } finally {
+ result.loadedResource.release();
+ result.scheduleChainedRequests();
+ }
+ } else if (mException != null) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "Asynchronous media loading failed, key=" +
+ mediaRequest.getKey(), mException);
+ if (bindableRequest != null) {
+ bindableRequest.onMediaResourceLoadError(bindableRequest, mException);
+ }
+ } else {
+ Assert.isTrue(bindableRequest == null || !bindableRequest.isBound());
+ if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(TAG, "media request not processed, no longer bound; key=" +
+ LogUtil.sanitizePII(mediaRequest.getKey()) /* key with phone# */);
+ }
+ }
+ }
+ };
+ mediaLoadingTask.executeOnExecutor(executor, (Void) null);
+ }
+
+ @VisibleForTesting
+ @RunsOnAnyThread
+ <T extends RefCountedMediaResource> void addResourceToMemoryCache(
+ final MediaRequest<T> mediaRequest, final T mediaResource) {
+ Assert.isTrue(mediaResource != null);
+ final MediaCache<T> mediaCache = mediaRequest.getMediaCache();
+ if (mediaCache != null) {
+ mediaCache.addResourceToCache(mediaRequest.getKey(), mediaResource);
+ if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(TAG, "added media resource to " + mediaCache.getName() + ". key=" +
+ LogUtil.sanitizePII(mediaRequest.getKey()) /* key can contain phone# */);
+ }
+ }
+ }
+
+ private class MediaLoadingResult<T extends RefCountedMediaResource> {
+ public final T loadedResource;
+ public final boolean fromCache;
+ private final List<MediaRequest<T>> mChainedRequests;
+
+ MediaLoadingResult(final T loadedResource, final boolean fromCache,
+ final List<MediaRequest<T>> chainedRequests) {
+ this.loadedResource = loadedResource;
+ this.fromCache = fromCache;
+ mChainedRequests = chainedRequests;
+ }
+
+ /**
+ * Asynchronously schedule a list of chained requests on the background thread.
+ */
+ public void scheduleChainedRequests() {
+ for (final MediaRequest<T> mediaRequest : mChainedRequests) {
+ scheduleAsyncMediaRequest(mediaRequest, MEDIA_BACKGROUND_EXECUTOR);
+ }
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/MessagePartImageRequestDescriptor.java b/src/com/android/messaging/datamodel/media/MessagePartImageRequestDescriptor.java
new file mode 100644
index 0000000..1871e66
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MessagePartImageRequestDescriptor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.net.Uri;
+
+import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.util.ImageUtils;
+
+/**
+ * Image descriptor attached to a message part.
+ * Once image size is determined during loading this descriptor will update the db if necessary.
+ */
+public class MessagePartImageRequestDescriptor extends UriImageRequestDescriptor {
+ private final String mMessagePartId;
+
+ /**
+ * Creates a new image request for a message part.
+ */
+ public MessagePartImageRequestDescriptor(final MessagePartData messagePart,
+ final int desiredWidth, final int desiredHeight, boolean isStatic) {
+ // Pull image parameters out of the MessagePart record
+ this(messagePart.getPartId(), messagePart.getContentUri(), desiredWidth, desiredHeight,
+ messagePart.getWidth(), messagePart.getHeight(), isStatic);
+ }
+
+ protected MessagePartImageRequestDescriptor(final String messagePartId, final Uri contentUri,
+ final int desiredWidth, final int desiredHeight, final int sourceWidth,
+ final int sourceHeight, boolean isStatic) {
+ super(contentUri, desiredWidth, desiredHeight, sourceWidth, sourceHeight,
+ true /* allowCompression */, isStatic, false /* cropToCircle */,
+ ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+ ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
+ mMessagePartId = messagePartId;
+ }
+
+ @Override
+ public void updateSourceDimensions(final int updatedWidth, final int updatedHeight) {
+ // If the dimensions of the image do not match then queue a DB update with new size.
+ // Don't update if we don't have a part id, which happens if this part is loaded as
+ // draft through actions such as share intent/message forwarding.
+ if (mMessagePartId != null &&
+ updatedWidth != MessagePartData.UNSPECIFIED_SIZE &&
+ updatedHeight != MessagePartData.UNSPECIFIED_SIZE &&
+ updatedWidth != sourceWidth && updatedHeight != sourceHeight) {
+ UpdateMessagePartSizeAction.updateSize(mMessagePartId, updatedWidth, updatedHeight);
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/MessagePartVideoThumbnailRequestDescriptor.java b/src/com/android/messaging/datamodel/media/MessagePartVideoThumbnailRequestDescriptor.java
new file mode 100644
index 0000000..ff11e92
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/MessagePartVideoThumbnailRequestDescriptor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.messaging.datamodel.data.MessagePartData;
+
+public class MessagePartVideoThumbnailRequestDescriptor extends MessagePartImageRequestDescriptor {
+ public MessagePartVideoThumbnailRequestDescriptor(MessagePartData messagePart) {
+ super(messagePart, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE, false);
+ }
+
+ public MessagePartVideoThumbnailRequestDescriptor(Uri uri) {
+ super(null, uri, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE,
+ ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE, false);
+ }
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
+ return new VideoThumbnailRequest(context, this);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/NetworkUriImageRequest.java b/src/com/android/messaging/datamodel/media/NetworkUriImageRequest.java
new file mode 100644
index 0000000..642e947
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/NetworkUriImageRequest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.messaging.Factory;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.ContentType;
+import com.android.messaging.util.LogUtil;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Serves network content URI based image requests.
+ */
+public class NetworkUriImageRequest<D extends UriImageRequestDescriptor> extends
+ ImageRequest<D> {
+
+ public NetworkUriImageRequest(Context context, D descriptor) {
+ super(context, descriptor);
+ mOrientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
+ }
+
+ @Override
+ protected InputStream getInputStreamForResource() throws FileNotFoundException {
+ Assert.isNotMainThread();
+ // Since we need to have an open urlConnection to get the stream, but we don't want to keep
+ // that connection open. There is no good way to perform this method.
+ return null;
+ }
+
+ @Override
+ protected boolean isGif() throws FileNotFoundException {
+ Assert.isNotMainThread();
+
+ HttpURLConnection connection = null;
+ try {
+ final URL url = new URL(mDescriptor.uri.toString());
+ connection = (HttpURLConnection) url.openConnection();
+ connection.connect();
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ return ContentType.IMAGE_GIF.equalsIgnoreCase(connection.getContentType());
+ }
+ } catch (MalformedURLException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG,
+ "MalformedUrl for image with url: "
+ + mDescriptor.uri.toString(), e);
+ } catch (IOException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG,
+ "IOException trying to get inputStream for image with url: "
+ + mDescriptor.uri.toString(), e);
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Bitmap loadBitmapInternal() throws IOException {
+ Assert.isNotMainThread();
+
+ InputStream inputStream = null;
+ Bitmap bitmap = null;
+ HttpURLConnection connection = null;
+ try {
+ final URL url = new URL(mDescriptor.uri.toString());
+ connection = (HttpURLConnection) url.openConnection();
+ connection.setDoInput(true);
+ connection.connect();
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ bitmap = BitmapFactory.decodeStream(connection.getInputStream());
+ }
+ } catch (MalformedURLException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG,
+ "MalformedUrl for image with url: "
+ + mDescriptor.uri.toString(), e);
+ } catch (final OutOfMemoryError e) {
+ LogUtil.e(LogUtil.BUGLE_TAG,
+ "OutOfMemoryError for image with url: "
+ + mDescriptor.uri.toString(), e);
+ Factory.get().reclaimMemory();
+ } catch (IOException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG,
+ "IOException trying to get inputStream for image with url: "
+ + mDescriptor.uri.toString(), e);
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/PoolableImageCache.java b/src/com/android/messaging/datamodel/media/PoolableImageCache.java
new file mode 100644
index 0000000..df814ba
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/PoolableImageCache.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.SparseArray;
+
+import com.android.messaging.Factory;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.LogUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+
+/**
+ * A media cache that holds image resources, which doubles as a bitmap pool that allows the
+ * consumer to optionally decode image resources using unused bitmaps stored in the cache.
+ */
+public class PoolableImageCache extends MediaCache<ImageResource> {
+ private static final int MIN_TIME_IN_POOL = 5000;
+
+ /** Encapsulates bitmap pool representation of the image cache */
+ private final ReusableImageResourcePool mReusablePoolAccessor = new ReusableImageResourcePool();
+
+ public PoolableImageCache(final int id, final String name) {
+ this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
+ }
+
+ public PoolableImageCache(final int maxSize, final int id, final String name) {
+ super(maxSize, id, name);
+ }
+
+ /**
+ * Creates a new BitmapFactory.Options for using the self-contained bitmap pool.
+ */
+ public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
+ final int inputDensity, final int targetDensity) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = scaled;
+ options.inDensity = inputDensity;
+ options.inTargetDensity = targetDensity;
+ options.inSampleSize = 1;
+ options.inJustDecodeBounds = false;
+ options.inMutable = true;
+ return options;
+ }
+
+ @Override
+ public synchronized ImageResource addResourceToCache(final String key,
+ final ImageResource imageResource) {
+ mReusablePoolAccessor.onResourceEnterCache(imageResource);
+ return super.addResourceToCache(key, imageResource);
+ }
+
+ @Override
+ protected synchronized void entryRemoved(final boolean evicted, final String key,
+ final ImageResource oldValue, final ImageResource newValue) {
+ mReusablePoolAccessor.onResourceLeaveCache(oldValue);
+ super.entryRemoved(evicted, key, oldValue, newValue);
+ }
+
+ /**
+ * Returns a representation of the image cache as a reusable bitmap pool.
+ */
+ public ReusableImageResourcePool asReusableBitmapPool() {
+ return mReusablePoolAccessor;
+ }
+
+ /**
+ * A bitmap pool representation built on top of the image cache. It treats the image resources
+ * stored in the image cache as a self-contained bitmap pool and is able to create or
+ * reclaim bitmap resource as needed.
+ */
+ public class ReusableImageResourcePool {
+ private static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
+ private static final int INVALID_POOL_KEY = 0;
+
+ /**
+ * Number of reuse failures to skip before reporting.
+ * For debugging purposes, change to a lower number for more frequent reporting.
+ */
+ private static final int FAILED_REPORTING_FREQUENCY = 100;
+
+ /**
+ * Count of reuse failures which have occurred.
+ */
+ private volatile int mFailedBitmapReuseCount = 0;
+
+ /**
+ * Count of reuse successes which have occurred.
+ */
+ private volatile int mSucceededBitmapReuseCount = 0;
+
+ /**
+ * A sparse array from bitmap size to a list of image cache entries that match the
+ * given size. This map is used to quickly retrieve a usable bitmap to be reused by an
+ * incoming ImageRequest. We need to ensure that this sparse array always contains only
+ * elements currently in the image cache with no other consumer.
+ */
+ private final SparseArray<LinkedList<ImageResource>> mImageListSparseArray;
+
+ public ReusableImageResourcePool() {
+ mImageListSparseArray = new SparseArray<LinkedList<ImageResource>>();
+ }
+
+ /**
+ * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce
+ * memory turnover.
+ * @param inputStream InputStream load. Cannot be null.
+ * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool().
+ * Cannot be null.
+ * @param width The width of the bitmap.
+ * @param height The height of the bitmap.
+ * @return The decoded Bitmap with the resource drawn in it.
+ * @throws IOException
+ */
+ public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
+ @NonNull final BitmapFactory.Options optionsTmp,
+ final int width, final int height) throws IOException {
+ if (width <= 0 || height <= 0) {
+ // This is an invalid / corrupted image of zero size.
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
+ "invalid size");
+ throw new IOException("Invalid size / corrupted image");
+ }
+ Assert.notNull(inputStream);
+ assignPoolBitmap(optionsTmp, width, height);
+ Bitmap b = null;
+ try {
+ b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
+ mSucceededBitmapReuseCount++;
+ } catch (final IllegalArgumentException e) {
+ // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
+ if (optionsTmp.inBitmap != null) {
+ optionsTmp.inBitmap.recycle();
+ optionsTmp.inBitmap = null;
+ b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
+ onFailedToReuse();
+ }
+ } catch (final OutOfMemoryError e) {
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
+ Factory.get().reclaimMemory();
+ }
+ return b;
+ }
+
+ /**
+ * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce
+ * memory turnover.
+ * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
+ * @param optionsTmp The bitmap will set here and the input should be generated from
+ * getBitmapOptionsForPool(). Cannot be null.
+ * @param width The width of the bitmap.
+ * @param height The height of the bitmap.
+ * @return A Bitmap with the encoded bytes drawn in it.
+ * @throws IOException
+ */
+ public Bitmap decodeByteArray(@NonNull final byte[] bytes,
+ @NonNull final BitmapFactory.Options optionsTmp, final int width,
+ final int height) throws OutOfMemoryError, IOException {
+ if (width <= 0 || height <= 0) {
+ // This is an invalid / corrupted image of zero size.
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
+ "invalid size");
+ throw new IOException("Invalid size / corrupted image");
+ }
+ Assert.notNull(bytes);
+ Assert.notNull(optionsTmp);
+ assignPoolBitmap(optionsTmp, width, height);
+ Bitmap b = null;
+ try {
+ b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
+ mSucceededBitmapReuseCount++;
+ } catch (final IllegalArgumentException e) {
+ // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
+ // (i.e. without the bitmap from the pool)
+ if (optionsTmp.inBitmap != null) {
+ optionsTmp.inBitmap.recycle();
+ optionsTmp.inBitmap = null;
+ b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
+ onFailedToReuse();
+ }
+ } catch (final OutOfMemoryError e) {
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
+ Factory.get().reclaimMemory();
+ }
+ return b;
+ }
+
+ /**
+ * Called when a new image resource is added to the cache. We add the resource to the
+ * pool so it's properly keyed into the pool structure.
+ */
+ void onResourceEnterCache(final ImageResource imageResource) {
+ if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
+ addResourceToPool(imageResource);
+ }
+ }
+
+ /**
+ * Called when an image resource is evicted from the cache. Bitmap pool's entries are
+ * strictly tied to their presence in the image cache. Once an image is evicted from the
+ * cache, it should be removed from the pool.
+ */
+ void onResourceLeaveCache(final ImageResource imageResource) {
+ if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
+ removeResourceFromPool(imageResource);
+ }
+ }
+
+ private void addResourceToPool(final ImageResource imageResource) {
+ synchronized (PoolableImageCache.this) {
+ final int poolKey = getPoolKey(imageResource);
+ Assert.isTrue(poolKey != INVALID_POOL_KEY);
+ LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
+ if (imageList == null) {
+ imageList = new LinkedList<ImageResource>();
+ mImageListSparseArray.put(poolKey, imageList);
+ }
+ imageList.addLast(imageResource);
+ }
+ }
+
+ private void removeResourceFromPool(final ImageResource imageResource) {
+ synchronized (PoolableImageCache.this) {
+ final int poolKey = getPoolKey(imageResource);
+ Assert.isTrue(poolKey != INVALID_POOL_KEY);
+ final LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
+ if (imageList != null) {
+ imageList.remove(imageResource);
+ }
+ }
+ }
+
+ /**
+ * Try to get a reusable bitmap from the pool with the given width and height. As a
+ * result of this call, the caller will assume ownership of the returned bitmap.
+ */
+ private Bitmap getReusableBitmapFromPool(final int width, final int height) {
+ synchronized (PoolableImageCache.this) {
+ final int poolKey = getPoolKey(width, height);
+ if (poolKey != INVALID_POOL_KEY) {
+ final LinkedList<ImageResource> images = mImageListSparseArray.get(poolKey);
+ if (images != null && images.size() > 0) {
+ // Try to reuse the first available bitmap from the pool list. We start from
+ // the least recently added cache entry of the given size.
+ ImageResource imageToUse = null;
+ for (int i = 0; i < images.size(); i++) {
+ final ImageResource image = images.get(i);
+ if (image.getRefCount() == 1) {
+ image.acquireLock();
+ if (image.getRefCount() == 1) {
+ // The image is only used by the cache, so it's reusable.
+ imageToUse = images.remove(i);
+ break;
+ } else {
+ // Logically, this shouldn't happen, because as soon as the
+ // cache is the only user of this resource, it will not be
+ // used by anyone else until the next cache access, but we
+ // currently hold on to the cache lock. But technically
+ // future changes may violate this assumption, so warn about
+ // this.
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Image refCount changed " +
+ "from 1 in getReusableBitmapFromPool()");
+ image.releaseLock();
+ }
+ }
+ }
+
+ if (imageToUse == null) {
+ return null;
+ }
+
+ try {
+ imageToUse.assertLockHeldByCurrentThread();
+
+ // Only reuse the bitmap if the last time we use was greater than 5s.
+ // This allows the cache a chance to reuse instead of always taking the
+ // oldest.
+ final long timeSinceLastRef = SystemClock.elapsedRealtime() -
+ imageToUse.getLastRefAddTimestamp();
+ if (timeSinceLastRef < MIN_TIME_IN_POOL) {
+ if (LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE)) {
+ LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "Not reusing reusing " +
+ "first available bitmap from the pool because it " +
+ "has not been in the pool long enough. " +
+ "timeSinceLastRef=" + timeSinceLastRef);
+ }
+ // Put back the image and return no reuseable bitmap.
+ images.addLast(imageToUse);
+ return null;
+ }
+
+ // Add a temp ref on the image resource so it won't be GC'd after
+ // being removed from the cache.
+ imageToUse.addRef();
+
+ // Remove the image resource from the image cache.
+ final ImageResource removed = remove(imageToUse.getKey());
+ Assert.isTrue(removed == imageToUse);
+
+ // Try to reuse the bitmap from the image resource. This will transfer
+ // ownership of the bitmap object to the caller of this method.
+ final Bitmap reusableBitmap = imageToUse.reuseBitmap();
+
+ imageToUse.release();
+ return reusableBitmap;
+ } finally {
+ // We are either done with the reuse operation, or decided not to use
+ // the image. Either way, release the lock.
+ imageToUse.releaseLock();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
+ * @param width desired bitmap width
+ * @param height desired bitmap height
+ * @return the created or reused mutable bitmap that has its background cleared to
+ * {@value Color#TRANSPARENT}
+ */
+ public Bitmap createOrReuseBitmap(final int width, final int height) {
+ return createOrReuseBitmap(width, height, Color.TRANSPARENT);
+ }
+
+ /**
+ * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
+ * @param width desired bitmap width
+ * @param height desired bitmap height
+ * @param backgroundColor the background color for the returned bitmap
+ * @return the created or reused mutable bitmap with the requested background color
+ */
+ public Bitmap createOrReuseBitmap(final int width, final int height,
+ final int backgroundColor) {
+ Bitmap retBitmap = null;
+ try {
+ final Bitmap poolBitmap = getReusableBitmapFromPool(width, height);
+ retBitmap = (poolBitmap != null) ? poolBitmap :
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ retBitmap.eraseColor(backgroundColor);
+ } catch (final OutOfMemoryError e) {
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache:try to createOrReuseBitmap");
+ Factory.get().reclaimMemory();
+ }
+ return retBitmap;
+ }
+
+ private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
+ final int height) {
+ if (optionsTmp.inJustDecodeBounds) {
+ return;
+ }
+ optionsTmp.inBitmap = getReusableBitmapFromPool(width, height);
+ }
+
+ /**
+ * @return The pool key for the provided image dimensions or 0 if either width or height is
+ * greater than the max supported image dimension.
+ */
+ private int getPoolKey(final int width, final int height) {
+ if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
+ return INVALID_POOL_KEY;
+ }
+ return (width << 16) | height;
+ }
+
+ /**
+ * @return the pool key for a given image resource.
+ */
+ private int getPoolKey(final ImageResource imageResource) {
+ if (imageResource.supportsBitmapReuse()) {
+ final Bitmap bitmap = imageResource.getBitmap();
+ if (bitmap != null && bitmap.isMutable()) {
+ final int width = bitmap.getWidth();
+ final int height = bitmap.getHeight();
+ if (width > 0 && height > 0) {
+ return getPoolKey(width, height);
+ }
+ }
+ }
+ return INVALID_POOL_KEY;
+ }
+
+ /**
+ * Called when bitmap reuse fails. Conditionally report the failure with statistics.
+ */
+ private void onFailedToReuse() {
+ mFailedBitmapReuseCount++;
+ if (mFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
+ LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
+ "Pooled bitmap consistently not being reused. Failure count = " +
+ mFailedBitmapReuseCount + ", success count = " +
+ mSucceededBitmapReuseCount);
+ }
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/RefCountedMediaResource.java b/src/com/android/messaging/datamodel/media/RefCountedMediaResource.java
new file mode 100644
index 0000000..c21f477
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/RefCountedMediaResource.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.os.SystemClock;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.LogUtil;
+import com.google.common.base.Throwables;
+
+import java.util.ArrayList;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A ref-counted class that holds loaded media resource, be it bitmaps or media bytes.
+ * Subclasses must implement the close() method to release any resources (such as bitmaps)
+ * when it's no longer used.
+ *
+ * Instances of the subclasses are:
+ * 1. Loaded by their corresponding MediaRequest classes.
+ * 2. Maintained by MediaResourceManager in its MediaCache pool.
+ * 3. Used by the UI (such as ContactIconViews) to present the content.
+ *
+ * Note: all synchronized methods in this class (e.g. addRef()) should not attempt to make outgoing
+ * calls that could potentially acquire media cache locks due to the potential deadlock this can
+ * cause. To synchronize read/write access to shared resource, {@link #acquireLock()} and
+ * {@link #releaseLock()} must be used, instead of using synchronized keyword.
+ */
+public abstract class RefCountedMediaResource {
+ private final String mKey;
+ private int mRef = 0;
+ private long mLastRefAddTimestamp;
+
+ // Set DEBUG to true to enable detailed stack trace for each addRef() and release() operation
+ // to find out where each ref change happens.
+ private static final boolean DEBUG = false;
+ private static final String TAG = "bugle_media_ref_history";
+ private final ArrayList<String> mRefHistory = new ArrayList<String>();
+
+ // A lock that guards access to shared members in this class (and all its subclasses).
+ private final ReentrantLock mLock = new ReentrantLock();
+
+ public RefCountedMediaResource(final String key) {
+ mKey = key;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public void addRef() {
+ acquireLock();
+ try {
+ if (DEBUG) {
+ mRefHistory.add("Added ref current ref = " + mRef);
+ mRefHistory.add(Throwables.getStackTraceAsString(new Exception()));
+ }
+
+ mRef++;
+ mLastRefAddTimestamp = SystemClock.elapsedRealtime();
+ } finally {
+ releaseLock();
+ }
+ }
+
+ public void release() {
+ acquireLock();
+ try {
+ if (DEBUG) {
+ mRefHistory.add("Released ref current ref = " + mRef);
+ mRefHistory.add(Throwables.getStackTraceAsString(new Exception()));
+ }
+
+ mRef--;
+ if (mRef == 0) {
+ close();
+ } else if (mRef < 0) {
+ if (DEBUG) {
+ LogUtil.i(TAG, "Unwinding ref count history for RefCountedMediaResource "
+ + this);
+ for (final String ref : mRefHistory) {
+ LogUtil.i(TAG, ref);
+ }
+ }
+ Assert.fail("RefCountedMediaResource has unbalanced ref. Refcount=" + mRef);
+ }
+ } finally {
+ releaseLock();
+ }
+ }
+
+ public int getRefCount() {
+ acquireLock();
+ try {
+ return mRef;
+ } finally {
+ releaseLock();
+ }
+ }
+
+ public long getLastRefAddTimestamp() {
+ acquireLock();
+ try {
+ return mLastRefAddTimestamp;
+ } finally {
+ releaseLock();
+ }
+ }
+
+ public void assertSingularRefCount() {
+ acquireLock();
+ try {
+ Assert.equals(1, mRef);
+ } finally {
+ releaseLock();
+ }
+ }
+
+ void acquireLock() {
+ mLock.lock();
+ }
+
+ void releaseLock() {
+ mLock.unlock();
+ }
+
+ void assertLockHeldByCurrentThread() {
+ Assert.isTrue(mLock.isHeldByCurrentThread());
+ }
+
+ boolean isEncoded() {
+ return false;
+ }
+
+ boolean isCacheable() {
+ return true;
+ }
+
+ MediaRequest<? extends RefCountedMediaResource> getMediaDecodingRequest(
+ final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
+ return null;
+ }
+
+ MediaRequest<? extends RefCountedMediaResource> getMediaEncodingRequest(
+ final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
+ return null;
+ }
+
+ public abstract int getMediaSize();
+ protected abstract void close();
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/SimSelectorAvatarRequest.java b/src/com/android/messaging/datamodel/media/SimSelectorAvatarRequest.java
new file mode 100644
index 0000000..e4f0334
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/SimSelectorAvatarRequest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.ExifInterface;
+import android.text.TextUtils;
+
+import com.android.messaging.R;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SimSelectorAvatarRequest extends AvatarRequest {
+ private static Bitmap sRegularSimIcon;
+
+ public SimSelectorAvatarRequest(final Context context,
+ final AvatarRequestDescriptor descriptor) {
+ super(context, descriptor);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTasks)
+ throws IOException {
+ Assert.isNotMainThread();
+ final String avatarType = AvatarUriUtil.getAvatarType(mDescriptor.uri);
+ if (AvatarUriUtil.TYPE_SIM_SELECTOR_URI.equals(avatarType)){
+ final int width = mDescriptor.desiredWidth;
+ final int height = mDescriptor.desiredHeight;
+ final String identifier = AvatarUriUtil.getIdentifier(mDescriptor.uri);
+ final boolean simSelected = AvatarUriUtil.getSimSelected(mDescriptor.uri);
+ final int simColor = AvatarUriUtil.getSimColor(mDescriptor.uri);
+ final boolean incoming = AvatarUriUtil.getSimIncoming(mDescriptor.uri);
+ return renderSimAvatarInternal(identifier, width, height, simColor, simSelected,
+ incoming);
+ }
+ return super.loadMediaInternal(chainedTasks);
+ }
+
+ private ImageResource renderSimAvatarInternal(final String identifier, final int width,
+ final int height, final int subColor, final boolean selected, final boolean incoming) {
+ final Resources resources = mContext.getResources();
+ final float halfWidth = width / 2;
+ final float halfHeight = height / 2;
+ final int minOfWidthAndHeight = Math.min(width, height);
+ final int backgroundColor = selected ? subColor : Color.WHITE;
+ final int textColor = selected ? subColor : Color.WHITE;
+ final int simColor = selected ? Color.WHITE : subColor;
+ final Bitmap bitmap = getBitmapPool().createOrReuseBitmap(width, height, backgroundColor);
+ final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ final Canvas canvas = new Canvas(bitmap);
+
+ if (sRegularSimIcon == null) {
+ final BitmapDrawable regularSim = (BitmapDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ic_sim_card_send);
+ sRegularSimIcon = regularSim.getBitmap();
+ }
+
+ paint.setColorFilter(new PorterDuffColorFilter(simColor, PorterDuff.Mode.SRC_ATOP));
+ paint.setAlpha(0xff);
+ canvas.drawBitmap(sRegularSimIcon, halfWidth - sRegularSimIcon.getWidth() / 2,
+ halfHeight - sRegularSimIcon.getHeight() / 2, paint);
+ paint.setColorFilter(null);
+ paint.setAlpha(0xff);
+
+ if (!TextUtils.isEmpty(identifier)) {
+ paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
+ paint.setColor(textColor);
+ final float letterToTileRatio =
+ resources.getFraction(R.dimen.sim_identifier_to_tile_ratio, 1, 1);
+ paint.setTextSize(letterToTileRatio * minOfWidthAndHeight);
+
+ final String firstCharString = identifier.substring(0, 1).toUpperCase();
+ final Rect textBound = new Rect();
+ paint.getTextBounds(firstCharString, 0, 1, textBound);
+
+ final float xOffset = halfWidth - textBound.centerX();
+ final float yOffset = halfHeight - textBound.centerY();
+ canvas.drawText(firstCharString, xOffset, yOffset, paint);
+ }
+
+ return new DecodedImageResource(getKey(), bitmap, ExifInterface.ORIENTATION_NORMAL);
+ }
+
+ @Override
+ public int getCacheId() {
+ return BugleMediaCacheManager.AVATAR_IMAGE_CACHE;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/UriImageRequest.java b/src/com/android/messaging/datamodel/media/UriImageRequest.java
new file mode 100644
index 0000000..b4934ca
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/UriImageRequest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Serves local content URI based image requests.
+ */
+public class UriImageRequest<D extends UriImageRequestDescriptor> extends ImageRequest<D> {
+ public UriImageRequest(final Context context, final D descriptor) {
+ super(context, descriptor);
+ }
+
+ @Override
+ protected InputStream getInputStreamForResource() throws FileNotFoundException {
+ return mContext.getContentResolver().openInputStream(mDescriptor.uri);
+ }
+
+ @Override
+ protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTasks)
+ throws IOException {
+ final ImageResource resource = super.loadMediaInternal(chainedTasks);
+ // Check if the caller asked for compression. If so, chain an encoding task if possible.
+ if (mDescriptor.allowCompression && chainedTasks != null) {
+ @SuppressWarnings("unchecked")
+ final MediaRequest<ImageResource> chainedTask = (MediaRequest<ImageResource>)
+ resource.getMediaEncodingRequest(this);
+ if (chainedTask != null) {
+ chainedTasks.add(chainedTask);
+ // Don't cache decoded image resource since we'll perform compression and cache
+ // the compressed resource.
+ if (resource instanceof DecodedImageResource) {
+ ((DecodedImageResource) resource).setCacheable(false);
+ }
+ }
+ }
+ return resource;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/UriImageRequestDescriptor.java b/src/com/android/messaging/datamodel/media/UriImageRequestDescriptor.java
new file mode 100644
index 0000000..c5685d1
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/UriImageRequestDescriptor.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.messaging.util.UriUtil;
+
+public class UriImageRequestDescriptor extends ImageRequestDescriptor {
+ public final Uri uri;
+ public final boolean allowCompression;
+
+ public UriImageRequestDescriptor(final Uri uri) {
+ this(uri, UriImageRequest.UNSPECIFIED_SIZE, UriImageRequest.UNSPECIFIED_SIZE, false, false,
+ false, 0, 0);
+ }
+
+ public UriImageRequestDescriptor(final Uri uri, final int desiredWidth, final int desiredHeight)
+ {
+ this(uri, desiredWidth, desiredHeight, false, false, false, 0, 0);
+ }
+
+ public UriImageRequestDescriptor(final Uri uri, final int desiredWidth, final int desiredHeight,
+ final boolean cropToCircle, final int circleBackgroundColor, int circleStrokeColor)
+ {
+ this(uri, desiredWidth, desiredHeight, false,
+ false, cropToCircle, circleBackgroundColor, circleStrokeColor);
+ }
+
+ public UriImageRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight, final boolean allowCompression, boolean isStatic,
+ boolean cropToCircle, int circleBackgroundColor, int circleStrokeColor) {
+ this(uri, desiredWidth, desiredHeight, UriImageRequest.UNSPECIFIED_SIZE,
+ UriImageRequest.UNSPECIFIED_SIZE, allowCompression, isStatic, cropToCircle,
+ circleBackgroundColor, circleStrokeColor);
+ }
+
+ /**
+ * Creates a new Uri-based image request.
+ * @param uri the content Uri. Currently Bugle only supports local resources Uri (i.e. it has
+ * to begin with content: or android.resource:
+ * @param circleStrokeColor
+ */
+ public UriImageRequestDescriptor(final Uri uri, final int desiredWidth,
+ final int desiredHeight, final int sourceWidth, final int sourceHeight,
+ final boolean allowCompression, final boolean isStatic, final boolean cropToCircle,
+ final int circleBackgroundColor, int circleStrokeColor) {
+ super(desiredWidth, desiredHeight, sourceWidth, sourceHeight, isStatic,
+ cropToCircle, circleBackgroundColor, circleStrokeColor);
+ this.uri = uri;
+ this.allowCompression = allowCompression;
+ }
+
+ @Override
+ public String getKey() {
+ if (uri != null) {
+ final String key = super.getKey();
+ if (key != null) {
+ return new StringBuilder()
+ .append(uri).append(KEY_PART_DELIMITER)
+ .append(String.valueOf(allowCompression)).append(KEY_PART_DELIMITER)
+ .append(key).toString();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
+ if (uri == null || UriUtil.isLocalUri(uri)) {
+ return new UriImageRequest<UriImageRequestDescriptor>(context, this);
+ } else {
+ return new NetworkUriImageRequest<UriImageRequestDescriptor>(context, this);
+ }
+ }
+
+ /** ID of the resource in MediaStore or null if this resource didn't come from MediaStore */
+ public Long getMediaStoreId() {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/media/VCardRequest.java b/src/com/android/messaging/datamodel/media/VCardRequest.java
new file mode 100644
index 0000000..d6e992c
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VCardRequest.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.PhoneUtils;
+import com.android.messaging.util.UriUtil;
+import com.android.vcard.VCardConfig;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardNotSupportedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Requests and parses VCard data. In Bugle, we need to display VCard details in the conversation
+ * view such as avatar icon and name, which can be expensive if we parse VCard every time.
+ * Therefore, we'd like to load the vcard once and cache it in our media cache using the
+ * MediaResourceManager component. To load the VCard, we use framework's VCard support to
+ * interpret the VCard content, which gives us information such as phone and email list, which
+ * we'll put in VCardResource object to be cached.
+ *
+ * Some particular attention is needed for the avatar icon. If the VCard contains avatar icon,
+ * it's in byte array form that can't easily be cached/persisted. Therefore, we persist the
+ * image bytes to the scratch directory and generate a content Uri for it, so that ContactIconView
+ * may use this Uri to display and cache the image if needed.
+ */
+public class VCardRequest implements MediaRequest<VCardResource> {
+ private final Context mContext;
+ private final VCardRequestDescriptor mDescriptor;
+ private final List<VCardResourceEntry> mLoadedVCards;
+ private VCardResource mLoadedResource;
+ private static final int VCARD_LOADING_TIMEOUT_MILLIS = 10000; // 10s
+ private static final String DEFAULT_VCARD_TYPE = "default";
+
+ VCardRequest(final Context context, final VCardRequestDescriptor descriptor) {
+ mDescriptor = descriptor;
+ mContext = context;
+ mLoadedVCards = new ArrayList<VCardResourceEntry>();
+ }
+
+ @Override
+ public String getKey() {
+ return mDescriptor.vCardUri.toString();
+ }
+
+ @Override
+ @DoesNotRunOnMainThread
+ public VCardResource loadMediaBlocking(List<MediaRequest<VCardResource>> chainedTask)
+ throws Exception {
+ Assert.isNotMainThread();
+ Assert.isTrue(mLoadedResource == null);
+ Assert.equals(0, mLoadedVCards.size());
+
+ // The VCard library doesn't support synchronously loading the media resource. Therefore,
+ // We have to burn the thread waiting for the result to come back.
+ final CountDownLatch signal = new CountDownLatch(1);
+ if (!parseVCard(mDescriptor.vCardUri, signal)) {
+ // Directly fail without actually going through the interpreter, return immediately.
+ throw new VCardException("Invalid vcard");
+ }
+
+ signal.await(VCARD_LOADING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ if (mLoadedResource == null) {
+ // Maybe null if failed or timeout.
+ throw new VCardException("Failure or timeout loading vcard");
+ }
+ return mLoadedResource;
+ }
+
+ @Override
+ public int getCacheId() {
+ return BugleMediaCacheManager.VCARD_CACHE;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public MediaCache<VCardResource> getMediaCache() {
+ return (MediaCache<VCardResource>) MediaCacheManager.get().getOrCreateMediaCacheById(
+ getCacheId());
+ }
+
+ @DoesNotRunOnMainThread
+ private boolean parseVCard(final Uri targetUri, final CountDownLatch signal) {
+ Assert.isNotMainThread();
+ final VCardEntryCounter counter = new VCardEntryCounter();
+ final VCardSourceDetector detector = new VCardSourceDetector();
+ boolean result;
+ try {
+ // We don't know which type should be used to parse the Uri.
+ // It is possible to misinterpret the vCard, but we expect the parser
+ // lets VCardSourceDetector detect the type before the misinterpretation.
+ result = readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN,
+ detector, true, null);
+ } catch (final VCardNestedException e) {
+ try {
+ final int estimatedVCardType = detector.getEstimatedType();
+ // Assume that VCardSourceDetector was able to detect the source.
+ // Try again with the detector.
+ result = readOneVCardFile(targetUri, estimatedVCardType,
+ counter, false, null);
+ } catch (final VCardNestedException e2) {
+ result = false;
+ LogUtil.e(LogUtil.BUGLE_TAG, "Must not reach here. " + e2);
+ }
+ }
+
+ if (!result) {
+ // Load failure.
+ return false;
+ }
+
+ return doActuallyReadOneVCard(targetUri, true, detector, null, signal);
+ }
+
+ @DoesNotRunOnMainThread
+ private boolean doActuallyReadOneVCard(final Uri uri, final boolean showEntryParseProgress,
+ final VCardSourceDetector detector, final List<String> errorFileNameList,
+ final CountDownLatch signal) {
+ Assert.isNotMainThread();
+ int vcardType = detector.getEstimatedType();
+ if (vcardType == VCardConfig.VCARD_TYPE_UNKNOWN) {
+ vcardType = VCardConfig.getVCardTypeFromString(DEFAULT_VCARD_TYPE);
+ }
+ final CustomVCardEntryConstructor builder =
+ new CustomVCardEntryConstructor(vcardType, null);
+ builder.addEntryHandler(new ContactVCardEntryHandler(signal));
+
+ try {
+ if (!readOneVCardFile(uri, vcardType, builder, false, null)) {
+ return false;
+ }
+ } catch (final VCardNestedException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "Must not reach here. " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @DoesNotRunOnMainThread
+ private boolean readOneVCardFile(final Uri uri, final int vcardType,
+ final VCardInterpreter interpreter,
+ final boolean throwNestedException, final List<String> errorFileNameList)
+ throws VCardNestedException {
+ Assert.isNotMainThread();
+ final ContentResolver resolver = mContext.getContentResolver();
+ VCardParser vCardParser;
+ InputStream is;
+ try {
+ is = resolver.openInputStream(uri);
+ vCardParser = new VCardParser_V21(vcardType);
+ vCardParser.addInterpreter(interpreter);
+
+ try {
+ vCardParser.parse(is);
+ } catch (final VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (final IOException e) {
+ // Do nothing.
+ }
+ if (interpreter instanceof CustomVCardEntryConstructor) {
+ // Let the object clean up internal temporal objects,
+ ((CustomVCardEntryConstructor) interpreter).clear();
+ }
+
+ is = resolver.openInputStream(uri);
+
+ try {
+ vCardParser = new VCardParser_V30(vcardType);
+ vCardParser.addInterpreter(interpreter);
+ vCardParser.parse(is);
+ } catch (final VCardVersionException e2) {
+ throw new VCardException("vCard with unspported version.");
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (final IOException e) {
+ // Do nothing.
+ }
+ }
+ }
+ } catch (final IOException e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "IOException was emitted: " + e.getMessage());
+
+ if (errorFileNameList != null) {
+ errorFileNameList.add(uri.toString());
+ }
+ return false;
+ } catch (final VCardNotSupportedException e) {
+ if ((e instanceof VCardNestedException) && throwNestedException) {
+ throw (VCardNestedException) e;
+ }
+ if (errorFileNameList != null) {
+ errorFileNameList.add(uri.toString());
+ }
+ return false;
+ } catch (final VCardException e) {
+ if (errorFileNameList != null) {
+ errorFileNameList.add(uri.toString());
+ }
+ return false;
+ }
+ return true;
+ }
+
+ class ContactVCardEntryHandler implements CustomVCardEntryConstructor.EntryHandler {
+ final CountDownLatch mSignal;
+
+ public ContactVCardEntryHandler(final CountDownLatch signal) {
+ mSignal = signal;
+ }
+
+ @Override
+ public void onStart() {
+ }
+
+ @Override
+ @DoesNotRunOnMainThread
+ public void onEntryCreated(final CustomVCardEntry entry) {
+ Assert.isNotMainThread();
+ final String displayName = entry.getDisplayName();
+ final List<VCardEntry.PhotoData> photos = entry.getPhotoList();
+ Uri avatarUri = null;
+ if (photos != null && photos.size() > 0) {
+ // The photo data is in bytes form, so we need to persist it in our temp directory
+ // so that ContactIconView can load it and display it later
+ // (and cache it, of course).
+ for (final VCardEntry.PhotoData photo : photos) {
+ final byte[] photoBytes = photo.getBytes();
+ if (photoBytes != null) {
+ final InputStream inputStream = new ByteArrayInputStream(photoBytes);
+ try {
+ avatarUri = UriUtil.persistContentToScratchSpace(inputStream);
+ if (avatarUri != null) {
+ // Just load the first avatar and be done. Want more? wait for V2.
+ break;
+ }
+ } finally {
+ try {
+ inputStream.close();
+ } catch (final IOException e) {
+ // Do nothing.
+ }
+ }
+ }
+ }
+ }
+
+ // Fall back to generated avatar.
+ if (avatarUri == null) {
+ String destination = null;
+ final List<VCardEntry.PhoneData> phones = entry.getPhoneList();
+ if (phones != null && phones.size() > 0) {
+ destination = PhoneUtils.getDefault().getCanonicalBySystemLocale(
+ phones.get(0).getNumber());
+ }
+
+ if (destination == null) {
+ final List<VCardEntry.EmailData> emails = entry.getEmailList();
+ if (emails != null && emails.size() > 0) {
+ destination = emails.get(0).getAddress();
+ }
+ }
+ avatarUri = AvatarUriUtil.createAvatarUri(null, displayName, destination, null);
+ }
+
+ // Add the loaded vcard to the list.
+ mLoadedVCards.add(new VCardResourceEntry(entry, avatarUri));
+ }
+
+ @Override
+ public void onEnd() {
+ // Finished loading all vCard entries, signal the loading thread to proceed with the
+ // result.
+ if (mLoadedVCards.size() > 0) {
+ mLoadedResource = new VCardResource(getKey(), mLoadedVCards);
+ }
+ mSignal.countDown();
+ }
+ }
+
+ @Override
+ public int getRequestType() {
+ return MediaRequest.REQUEST_LOAD_MEDIA;
+ }
+
+ @Override
+ public MediaRequestDescriptor<VCardResource> getDescriptor() {
+ return mDescriptor;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/VCardRequestDescriptor.java b/src/com/android/messaging/datamodel/media/VCardRequestDescriptor.java
new file mode 100644
index 0000000..4084851
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VCardRequestDescriptor.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.messaging.util.Assert;
+
+public class VCardRequestDescriptor extends MediaRequestDescriptor<VCardResource> {
+ public final Uri vCardUri;
+
+ public VCardRequestDescriptor(final Uri vCardUri) {
+ Assert.notNull(vCardUri);
+ this.vCardUri = vCardUri;
+ }
+
+ @Override
+ public MediaRequest<VCardResource> buildSyncMediaRequest(Context context) {
+ return new VCardRequest(context, this);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/VCardResource.java b/src/com/android/messaging/datamodel/media/VCardResource.java
new file mode 100644
index 0000000..edf5e88
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VCardResource.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import java.util.List;
+
+/**
+ * Holds cached information of VCard contact info.
+ * The temporarily persisted avatar icon Uri is tied to the VCardResource. As a result, whenever
+ * the VCardResource is no longer used (i.e. close() is called), we need to asynchronously
+ * delete the avatar image from temp storage since no one will have reference to the avatar Uri
+ * again. The next time the same VCard is displayed, since the old resource has been evicted from
+ * the memory cache, we'll load and persist the avatar icon again.
+ */
+public class VCardResource extends RefCountedMediaResource {
+ private final List<VCardResourceEntry> mVCards;
+
+ public VCardResource(final String key, final List<VCardResourceEntry> vcards) {
+ super(key);
+ mVCards = vcards;
+ }
+
+ public List<VCardResourceEntry> getVCards() {
+ return mVCards;
+ }
+
+ @Override
+ public int getMediaSize() {
+ // Instead of track VCards by size in kilobytes, we track them by count.
+ return 0;
+ }
+
+ @Override
+ protected void close() {
+ for (final VCardResourceEntry vcard : mVCards) {
+ vcard.close();
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/VCardResourceEntry.java b/src/com/android/messaging/datamodel/media/VCardResourceEntry.java
new file mode 100644
index 0000000..f76b796
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VCardResourceEntry.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v4.util.ArrayMap;
+import android.text.TextUtils;
+
+import com.android.messaging.Factory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.MediaScratchFileProvider;
+import com.android.messaging.datamodel.data.PersonItemData;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.SafeAsyncTask;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntry.EmailData;
+import com.android.vcard.VCardEntry.ImData;
+import com.android.vcard.VCardEntry.NoteData;
+import com.android.vcard.VCardEntry.OrganizationData;
+import com.android.vcard.VCardEntry.PhoneData;
+import com.android.vcard.VCardEntry.PostalData;
+import com.android.vcard.VCardEntry.WebsiteData;
+import com.android.vcard.VCardProperty;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Holds one entry item (i.e. a single contact) within a VCard resource. It is able to take
+ * a VCardEntry and extract relevant information from it.
+ */
+public class VCardResourceEntry {
+ public static final String PROPERTY_KIND = "KIND";
+
+ public static final String KIND_LOCATION = "location";
+
+ private final List<VCardResourceEntry.VCardResourceEntryDestinationItem> mContactInfo;
+ private final Uri mAvatarUri;
+ private final String mDisplayName;
+ private final CustomVCardEntry mVCard;
+
+ public VCardResourceEntry(final CustomVCardEntry vcard, final Uri avatarUri) {
+ mContactInfo = getContactInfoFromVCardEntry(vcard);
+ mDisplayName = getDisplayNameFromVCardEntry(vcard);
+ mAvatarUri = avatarUri;
+ mVCard = vcard;
+ }
+
+ void close() {
+ // If the avatar image was temporarily saved in the scratch folder, remove that.
+ if (MediaScratchFileProvider.isMediaScratchSpaceUri(mAvatarUri)) {
+ SafeAsyncTask.executeOnThreadPool(new Runnable() {
+ @Override
+ public void run() {
+ Factory.get().getApplicationContext().getContentResolver().delete(
+ mAvatarUri, null, null);
+ }
+ });
+ }
+ }
+
+ public String getKind() {
+ VCardProperty kindProperty = mVCard.getProperty(PROPERTY_KIND);
+ return kindProperty == null ? null : kindProperty.getRawValue();
+ }
+
+ public Uri getAvatarUri() {
+ return mAvatarUri;
+ }
+
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public String getDisplayAddress() {
+ List<PostalData> postalList = mVCard.getPostalList();
+ if (postalList == null || postalList.size() < 1) {
+ return null;
+ }
+
+ return formatAddress(postalList.get(0));
+ }
+
+ public String getNotes() {
+ List<NoteData> notes = mVCard.getNotes();
+ if (notes == null || notes.size() == 0) {
+ return null;
+ }
+ StringBuilder noteBuilder = new StringBuilder();
+ for (NoteData note : notes) {
+ noteBuilder.append(note.getNote());
+ }
+ return noteBuilder.toString();
+ }
+
+ /**
+ * Returns a UI-facing representation that can be bound and consumed by the UI layer to display
+ * this VCard resource entry.
+ */
+ public PersonItemData getDisplayItem() {
+ return new PersonItemData() {
+ @Override
+ public Uri getAvatarUri() {
+ return VCardResourceEntry.this.getAvatarUri();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return VCardResourceEntry.this.getDisplayName();
+ }
+
+ @Override
+ public String getDetails() {
+ return null;
+ }
+
+ @Override
+ public Intent getClickIntent() {
+ return null;
+ }
+
+ @Override
+ public long getContactId() {
+ return ContactUtil.INVALID_CONTACT_ID;
+ }
+
+ @Override
+ public String getLookupKey() {
+ return null;
+ }
+
+ @Override
+ public String getNormalizedDestination() {
+ return null;
+ }
+ };
+ }
+
+ public List<VCardResourceEntry.VCardResourceEntryDestinationItem> getContactInfo() {
+ return mContactInfo;
+ }
+
+ private static List<VCardResourceEntryDestinationItem> getContactInfoFromVCardEntry(
+ final VCardEntry vcard) {
+ final Resources resources = Factory.get().getApplicationContext().getResources();
+ final List<VCardResourceEntry.VCardResourceEntryDestinationItem> retList =
+ new ArrayList<VCardResourceEntry.VCardResourceEntryDestinationItem>();
+ if (vcard.getPhoneList() != null) {
+ for (final PhoneData phone : vcard.getPhoneList()) {
+ final Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse("tel:" + phone.getNumber()));
+ retList.add(new VCardResourceEntryDestinationItem(phone.getNumber(),
+ Phone.getTypeLabel(resources, phone.getType(), phone.getLabel()).toString(),
+ intent));
+ }
+ }
+
+ if (vcard.getEmailList() != null) {
+ for (final EmailData email : vcard.getEmailList()) {
+ final Intent intent = new Intent(Intent.ACTION_SENDTO);
+ intent.setData(Uri.parse("mailto:"));
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[] { email.getAddress() });
+ retList.add(new VCardResourceEntryDestinationItem(email.getAddress(),
+ Phone.getTypeLabel(resources, email.getType(),
+ email.getLabel()).toString(), intent));
+ }
+ }
+
+ if (vcard.getPostalList() != null) {
+ for (final PostalData postalData : vcard.getPostalList()) {
+ String type;
+ try {
+ type = resources.
+ getStringArray(android.R.array.postalAddressTypes)
+ [postalData.getType() - 1];
+ } catch (final NotFoundException ex) {
+ type = resources.getStringArray(android.R.array.postalAddressTypes)[2];
+ } catch (final Exception e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "createContactItem postal Exception:" + e);
+ type = resources.getStringArray(android.R.array.postalAddressTypes)[2];
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ final String address = formatAddress(postalData);
+ try {
+ intent.setData(Uri.parse("geo:0,0?q=" + URLEncoder.encode(address, "UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ intent = null;
+ }
+
+ retList.add(new VCardResourceEntryDestinationItem(address, type, intent));
+ }
+ }
+
+ if (vcard.getImList() != null) {
+ for (final ImData imData : vcard.getImList()) {
+ String type = null;
+ try {
+ type = resources.
+ getString(Im.getProtocolLabelResource(imData.getProtocol()));
+ } catch (final NotFoundException ex) {
+ // Do nothing since this implies an empty label.
+ }
+ retList.add(new VCardResourceEntryDestinationItem(imData.getAddress(), type, null));
+ }
+ }
+
+ if (vcard.getOrganizationList() != null) {
+ for (final OrganizationData organtization : vcard.getOrganizationList()) {
+ String type = null;
+ try {
+ type = resources.getString(Organization.getTypeLabelResource(
+ organtization.getType()));
+ } catch (final NotFoundException ex) {
+ //set other kind as "other"
+ type = resources.getStringArray(android.R.array.organizationTypes)[1];
+ } catch (final Exception e) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "createContactItem org Exception:" + e);
+ type = resources.getStringArray(android.R.array.organizationTypes)[1];
+ }
+ retList.add(new VCardResourceEntryDestinationItem(
+ organtization.getOrganizationName(), type, null));
+ }
+ }
+
+ if (vcard.getWebsiteList() != null) {
+ for (final WebsiteData web : vcard.getWebsiteList()) {
+ if (web != null && TextUtils.isGraphic(web.getWebsite())){
+ String website = web.getWebsite();
+ if (!website.startsWith("http://") && !website.startsWith("https://")) {
+ // Prefix required for parsing to end up with a scheme and result in
+ // navigation
+ website = "http://" + website;
+ }
+ final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
+ retList.add(new VCardResourceEntryDestinationItem(web.getWebsite(), null,
+ intent));
+ }
+ }
+ }
+
+ if (vcard.getBirthday() != null) {
+ final String birthday = vcard.getBirthday();
+ if (TextUtils.isGraphic(birthday)){
+ retList.add(new VCardResourceEntryDestinationItem(birthday,
+ resources.getString(R.string.vcard_detail_birthday_label), null));
+ }
+ }
+
+ if (vcard.getNotes() != null) {
+ for (final NoteData note : vcard.getNotes()) {
+ final ArrayMap<String, String> curChildMap = new ArrayMap<String, String>();
+ if (TextUtils.isGraphic(note.getNote())){
+ retList.add(new VCardResourceEntryDestinationItem(note.getNote(),
+ resources.getString(R.string.vcard_detail_notes_label), null));
+ }
+ }
+ }
+ return retList;
+ }
+
+ private static String formatAddress(final PostalData postalData) {
+ final StringBuilder sb = new StringBuilder();
+ final String poBox = postalData.getPobox();
+ if (!TextUtils.isEmpty(poBox)) {
+ sb.append(poBox);
+ sb.append(" ");
+ }
+ final String extendedAddress = postalData.getExtendedAddress();
+ if (!TextUtils.isEmpty(extendedAddress)) {
+ sb.append(extendedAddress);
+ sb.append(" ");
+ }
+ final String street = postalData.getStreet();
+ if (!TextUtils.isEmpty(street)) {
+ sb.append(street);
+ sb.append(" ");
+ }
+ final String localty = postalData.getLocalty();
+ if (!TextUtils.isEmpty(localty)) {
+ sb.append(localty);
+ sb.append(" ");
+ }
+ final String region = postalData.getRegion();
+ if (!TextUtils.isEmpty(region)) {
+ sb.append(region);
+ sb.append(" ");
+ }
+ final String postalCode = postalData.getPostalCode();
+ if (!TextUtils.isEmpty(postalCode)) {
+ sb.append(postalCode);
+ sb.append(" ");
+ }
+ final String country = postalData.getCountry();
+ if (!TextUtils.isEmpty(country)) {
+ sb.append(country);
+ }
+ return sb.toString();
+ }
+
+ private static String getDisplayNameFromVCardEntry(final VCardEntry vcard) {
+ String name = vcard.getDisplayName();
+ if (name == null) {
+ vcard.consolidateFields();
+ name = vcard.getDisplayName();
+ }
+ return name;
+ }
+
+ /**
+ * Represents one entry line (e.g. phone number and phone label) for a single contact. Each
+ * VCardResourceEntry may hold one or more VCardResourceEntryDestinationItem's.
+ */
+ public static class VCardResourceEntryDestinationItem {
+ private final String mDisplayDestination;
+ private final String mDestinationType;
+ private final Intent mClickIntent;
+ public VCardResourceEntryDestinationItem(final String displayDestination,
+ final String destinationType, final Intent clickIntent) {
+ mDisplayDestination = displayDestination;
+ mDestinationType = destinationType;
+ mClickIntent = clickIntent;
+ }
+
+ /**
+ * Returns a UI-facing representation that can be bound and consumed by the UI layer to
+ * display this VCard resource destination entry.
+ */
+ public PersonItemData getDisplayItem() {
+ return new PersonItemData() {
+ @Override
+ public Uri getAvatarUri() {
+ return null;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return mDisplayDestination;
+ }
+
+ @Override
+ public String getDetails() {
+ return mDestinationType;
+ }
+
+ @Override
+ public Intent getClickIntent() {
+ return mClickIntent;
+ }
+
+ @Override
+ public long getContactId() {
+ return ContactUtil.INVALID_CONTACT_ID;
+ }
+
+ @Override
+ public String getLookupKey() {
+ return null;
+ }
+
+ @Override
+ public String getNormalizedDestination() {
+ return null;
+ }
+ };
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/VideoThumbnailRequest.java b/src/com/android/messaging/datamodel/media/VideoThumbnailRequest.java
new file mode 100644
index 0000000..f17591c
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VideoThumbnailRequest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.provider.MediaStore.Video.Thumbnails;
+
+import com.android.messaging.Factory;
+import com.android.messaging.util.MediaMetadataRetrieverWrapper;
+import com.android.messaging.util.OsUtil;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Class to request a video thumbnail.
+ * Users of this class as responsible for checking {@link #shouldShowIncomingVideoThumbnails}
+ */
+public class VideoThumbnailRequest extends ImageRequest<UriImageRequestDescriptor> {
+
+ public VideoThumbnailRequest(final Context context,
+ final UriImageRequestDescriptor descriptor) {
+ super(context, descriptor);
+ }
+
+ public static boolean shouldShowIncomingVideoThumbnails() {
+ return OsUtil.isAtLeastM();
+ }
+
+ @Override
+ protected InputStream getInputStreamForResource() throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ protected boolean hasBitmapObject() {
+ return true;
+ }
+
+ @Override
+ protected Bitmap getBitmapForResource() throws IOException {
+ final Long mediaId = mDescriptor.getMediaStoreId();
+ Bitmap bitmap = null;
+ if (mediaId != null) {
+ final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
+ bitmap = Thumbnails.getThumbnail(cr, mediaId, Thumbnails.MICRO_KIND, null);
+ } else {
+ final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper();
+ try {
+ retriever.setDataSource(mDescriptor.uri);
+ bitmap = retriever.getFrameAtTime();
+ } finally {
+ retriever.release();
+ }
+ }
+ if (bitmap != null) {
+ mDescriptor.updateSourceDimensions(bitmap.getWidth(), bitmap.getHeight());
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/media/VideoThumbnailRequestDescriptor.java b/src/com/android/messaging/datamodel/media/VideoThumbnailRequestDescriptor.java
new file mode 100644
index 0000000..907bb8f
--- /dev/null
+++ b/src/com/android/messaging/datamodel/media/VideoThumbnailRequestDescriptor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.Context;
+
+import com.android.messaging.util.ImageUtils;
+import com.android.messaging.util.UriUtil;
+
+public class VideoThumbnailRequestDescriptor extends UriImageRequestDescriptor {
+ protected final long mMediaId;
+ public VideoThumbnailRequestDescriptor(final long id, String path, int desiredWidth,
+ int desiredHeight, int sourceWidth, int sourceHeight) {
+ super(UriUtil.getUriForResourceFile(path), desiredWidth, desiredHeight, sourceWidth,
+ sourceHeight, false /* canCompress */, false /* isStatic */,
+ false /* cropToCircle */,
+ ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+ ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
+ mMediaId = id;
+ }
+
+ @Override
+ public MediaRequest<ImageResource> buildSyncMediaRequest(Context context) {
+ return new VideoThumbnailRequest(context, this);
+ }
+
+ @Override
+ public Long getMediaStoreId() {
+ return mMediaId;
+ }
+}