Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -1,74 +0,0 @@
/*
* 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();
}
}
@@ -1,159 +0,0 @@
/*
* 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;
}
}
@@ -1,189 +0,0 @@
/*
* 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;
}
}
@@ -1,60 +0,0 @@
/*
* 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);
}
}
}
@@ -1,63 +0,0 @@
/*
* 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;
}
}
@@ -1,54 +0,0 @@
/*
* 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;
}
}
@@ -1,109 +0,0 @@
/*
* 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!");
}
}
@@ -1,61 +0,0 @@
/*
* 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);
}
}
@@ -1,48 +0,0 @@
/*
* 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);
}
}
@@ -1,133 +0,0 @@
/*
* 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);
}
}
@@ -1,254 +0,0 @@
/*
* 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();
}
}
}
@@ -1,162 +0,0 @@
/*
* 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;
}
}
}
@@ -1,108 +0,0 @@
/*
* 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();
}
}
@@ -1,68 +0,0 @@
/*
* 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);
}
}
@@ -1,110 +0,0 @@
/*
* 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();
}
}
}
@@ -1,258 +0,0 @@
/*
* 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;
}
}
@@ -1,113 +0,0 @@
/*
* 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) {
}
}
@@ -1,63 +0,0 @@
/*
* 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;
}
}
@@ -1,42 +0,0 @@
/*
* 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() {
}
}
@@ -1,113 +0,0 @@
/*
* 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;
}
}
@@ -1,69 +0,0 @@
/*
* 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);
}
@@ -1,70 +0,0 @@
/*
* 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();
}
@@ -1,38 +0,0 @@
/*
* 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);
}
}
@@ -1,325 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,64 +0,0 @@
/*
* 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);
}
}
}
@@ -1,38 +0,0 @@
/*
* 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);
}
}
@@ -1,120 +0,0 @@
/*
* 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;
}
}
@@ -1,419 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,164 +0,0 @@
/*
* 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();
}
@@ -1,117 +0,0 @@
/*
* 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;
}
}
@@ -1,58 +0,0 @@
/*
* 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;
}
}
@@ -1,95 +0,0 @@
/*
* 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;
}
}
@@ -1,328 +0,0 @@
/*
* 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;
}
}
@@ -1,35 +0,0 @@
/*
* 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);
}
}
@@ -1,52 +0,0 @@
/*
* 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();
}
}
}
@@ -1,389 +0,0 @@
/*
* 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;
}
};
}
}
}
@@ -1,78 +0,0 @@
/*
* 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;
}
}
@@ -1,44 +0,0 @@
/*
* 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;
}
}