Revert "Initial checkin of AOSP Messaging app."
This reverts commit 461a34b466.
Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-38
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user