Merge "Update hierarchical folders; implement this "the right way"" into jb-ub-mail
diff --git a/res/values/strings.xml b/res/values/strings.xml
index dfae210..6b68507 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -634,7 +634,7 @@
<!-- Message displayed when trying to play a video that isn't ready [CHAR LIMIT=100] -->
<string name="photo_view_video_not_ready">Video not available at this time. Please refresh.</string>
- <!-- Message displayed when displaying a place holder image (for photos & videos) [CHAR LIMIT=50] -->
+ <!-- Message displayed when displaying a place holder image (for photos & videos) [CHAR LIMIT=60] -->
<string name="photo_view_placeholder_image">Item not available at this time. Please refresh.</string>
<!-- Text shown when a photo fails to download. -->
diff --git a/src/com/android/mail/browse/MessageAttachmentTile.java b/src/com/android/mail/browse/MessageAttachmentTile.java
index 613e6f6..7a4b9bc 100644
--- a/src/com/android/mail/browse/MessageAttachmentTile.java
+++ b/src/com/android/mail/browse/MessageAttachmentTile.java
@@ -47,7 +47,7 @@
import com.android.mail.R;
import com.android.mail.photo.Intents;
import com.android.mail.photo.Intents.PhotoViewIntentBuilder;
-import com.android.mail.photo.util.MediaStoreUtils;
+import com.android.mail.photo.util.ImageUtils;
import com.android.mail.providers.Attachment;
import com.android.mail.providers.UIProvider.AttachmentColumns;
import com.android.mail.providers.UIProvider.AttachmentDestination;
@@ -462,7 +462,7 @@
* View an attachment by an application on device.
*/
private void sendViewIntent() {
- if (MediaStoreUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.contentType))) {
+ if (ImageUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.contentType))) {
final PhotoViewIntentBuilder builder =
Intents.newPhotoViewActivityIntentBuilder(getContext());
builder.setAlbumName(mAttachment.name)
diff --git a/src/com/android/mail/compose/QuotedTextView.java b/src/com/android/mail/compose/QuotedTextView.java
index 69c8296..6fe5c29 100644
--- a/src/com/android/mail/compose/QuotedTextView.java
+++ b/src/com/android/mail/compose/QuotedTextView.java
@@ -207,12 +207,9 @@
private void respondInline() {
// Copy the text in the quoted message to the body of the
// message after stripping the html.
- HtmlParser parser = new HtmlParser();
- HtmlDocument doc = parser.parse(getQuotedText().toString());
- HtmlTreeBuilder builder = new HtmlTreeBuilder();
- doc.accept(builder);
+ final String plainText = Utils.convertHtmlToPlainText(getQuotedText().toString());
if (mRespondInlineListener != null) {
- mRespondInlineListener.onRespondInline("\n" + builder.getTree().getPlainText());
+ mRespondInlineListener.onRespondInline("\n" + plainText);
}
// Set quoted text to unchecked and not visible.
updateCheckedState(false);
diff --git a/src/com/android/mail/photo/BaseFragmentActivity.java b/src/com/android/mail/photo/BaseFragmentActivity.java
index adfd663..e38f4f6 100644
--- a/src/com/android/mail/photo/BaseFragmentActivity.java
+++ b/src/com/android/mail/photo/BaseFragmentActivity.java
@@ -36,11 +36,9 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
-import android.widget.ImageView;
import android.widget.TextView;
import com.android.mail.R;
-import com.android.mail.photo.util.ImageCache;
import java.util.ArrayList;
@@ -763,11 +761,4 @@
return null;
}
-
- @Override
- protected void onResume() {
- super.onResume();
-
- ImageCache.getInstance(this).refresh();
- }
}
diff --git a/src/com/android/mail/photo/content/ImageRequest.java b/src/com/android/mail/photo/content/ImageRequest.java
deleted file mode 100644
index 648e769..0000000
--- a/src/com/android/mail/photo/content/ImageRequest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.content;
-
-/**
- * A request for an image.
- */
-public abstract class ImageRequest {
-
- /**
- * @return true if this request is known to resolve to a missing image.
- */
- public abstract boolean isEmpty();
-}
diff --git a/src/com/android/mail/photo/content/LocalImageRequest.java b/src/com/android/mail/photo/content/LocalImageRequest.java
deleted file mode 100644
index fa30bb2..0000000
--- a/src/com/android/mail/photo/content/LocalImageRequest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2012 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.content;
-
-import android.net.Uri;
-
-/**
- * A request for a media image of a specific size.
- */
-public class LocalImageRequest extends ImageRequest {
- private final Uri mUri;
- private final int mWidth;
- private final int mHeight;
-
- private int mHashCode;
-
- public LocalImageRequest(int width, int height) {
- mUri = null;
- mWidth = width;
- mHeight = height;
- }
-
- /**
- * @return the original Uri
- */
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * @return the width
- */
- public int getWidth() {
- return mWidth;
- }
-
- /**
- * @return the height
- */
- public int getHeight() {
- return mHeight;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isEmpty() {
- return mUri == null;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int hashCode() {
- if (mHashCode == 0) {
- int result = 17;
- result = 31 * result + mUri.hashCode();
- result = 31 * result + mWidth;
- result = 31 * result + mHeight;
- mHashCode = result;
- }
- return mHashCode;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
-
- if (!(o instanceof LocalImageRequest)) {
- return false;
- }
-
- final LocalImageRequest other = (LocalImageRequest) o;
- return (mUri.equals(other.mUri) &&
- mWidth == other.mWidth &&
- mHeight == other.mHeight);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- return "LocalImageRequest: " + mUri.toString() + " (" + mWidth + ", " + mHeight + ")";
- }
-}
diff --git a/src/com/android/mail/photo/content/MediaImageRequest.java b/src/com/android/mail/photo/content/MediaImageRequest.java
deleted file mode 100644
index 612dc7d..0000000
--- a/src/com/android/mail/photo/content/MediaImageRequest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2012 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.content;
-
-import android.text.TextUtils;
-
-import com.android.mail.photo.util.ImageUtils;
-
-/**
- * A request for a media image of a specific size.
- */
-public class MediaImageRequest extends ImageRequest {
-
- private final String mUrl;
- private final String mMediaType;
- private final int mWidth;
- private final int mHeight;
- private final boolean mCropAndResize;
-
- private String mDownloadUrl;
- private int mHashCode;
-
- public MediaImageRequest() {
- this(null, null, 0, 0, false);
- }
-
- public MediaImageRequest(String url, String mediaType, int size) {
- this(url, mediaType, size, size, true);
- }
-
- public MediaImageRequest(
- String url, String mediaType, int width, int height, boolean cropAndResize) {
- if (url == null) {
- throw new NullPointerException();
- }
-
- mUrl = url;
- mMediaType = mediaType;
- mWidth = width;
- mHeight = height;
- mCropAndResize = cropAndResize;
- }
-
- /**
- * @return the original URL
- */
- public String getUrl() {
- return mUrl;
- }
-
- /**
- * @return the URL
- */
- public String getDownloadUrl() {
- if (mDownloadUrl == null) {
- if (!mCropAndResize || mWidth == 0) {
- mDownloadUrl = mUrl;
- } else if (mWidth == mHeight) {
- mDownloadUrl = ImageUtils.getCroppedAndResizedUrl(mWidth, mUrl);
- } else {
- mDownloadUrl = ImageUtils.getCenterCroppedAndResizedUrl(mWidth, mHeight, mUrl);
- }
-
- if (mDownloadUrl.startsWith("//")) {
- mDownloadUrl = "http:" + mDownloadUrl;
- }
- }
- return mDownloadUrl;
- }
-
- /**
- * @return the media type
- */
- public String getMediaType() {
- return mMediaType;
- }
-
- /**
- * @return the width
- */
- public int getWidth() {
- return mWidth;
- }
-
- /**
- * @return the height
- */
- public int getHeight() {
- return mHeight;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isEmpty() {
- return mUrl == null;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int hashCode() {
- if (mHashCode == 0) {
- if (mUrl != null) {
- mHashCode = mUrl.hashCode();
- } else {
- mHashCode = 1;
- }
- }
- return mHashCode;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof MediaImageRequest)) {
- return false;
- }
-
- MediaImageRequest k = (MediaImageRequest) o;
- return mWidth == k.mWidth && mHeight == k.mHeight
- && TextUtils.equals(mUrl, k.mUrl)
- && TextUtils.equals(mMediaType, k.mMediaType);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- return "MediaImageRequest: " + mMediaType + " " + mUrl + " (" + mWidth
- + ", " + mHeight + ")";
- }
-}
diff --git a/src/com/android/mail/photo/util/GifDrawable.java b/src/com/android/mail/photo/graphics/GifDrawable.java
similarity index 99%
rename from src/com/android/mail/photo/util/GifDrawable.java
rename to src/com/android/mail/photo/graphics/GifDrawable.java
index 5c625ea..821794f 100644
--- a/src/com/android/mail/photo/util/GifDrawable.java
+++ b/src/com/android/mail/photo/graphics/GifDrawable.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package com.android.mail.photo.util;
+package com.android.mail.photo.graphics;
import android.graphics.Bitmap;
import android.graphics.Canvas;
diff --git a/src/com/android/mail/photo/loaders/PhotoBitmapLoader.java b/src/com/android/mail/photo/loaders/PhotoBitmapLoader.java
index 49965a5..a1c8743 100644
--- a/src/com/android/mail/photo/loaders/PhotoBitmapLoader.java
+++ b/src/com/android/mail/photo/loaders/PhotoBitmapLoader.java
@@ -30,22 +30,22 @@
* Loader for the bitmap of a photo.
*/
public class PhotoBitmapLoader extends AsyncTaskLoader<Bitmap> {
- private final String mPhotoUrl;
+ private final String mPhotoUri;
private Bitmap mBitmap;
- public PhotoBitmapLoader(Context context, String photoUrl) {
+ public PhotoBitmapLoader(Context context, String photoUri) {
super(context);
- mPhotoUrl = photoUrl;
+ mPhotoUri = photoUri;
}
@Override
public Bitmap loadInBackground() {
Context context = getContext();
- if (context != null && mPhotoUrl != null) {
+ if (context != null && mPhotoUri != null) {
final ContentResolver resolver = context.getContentResolver();
- return ImageUtils.createLocalBitmap(resolver, Uri.parse(mPhotoUrl),
+ return ImageUtils.createLocalBitmap(resolver, Uri.parse(mPhotoUri),
PhotoViewFragment.sPhotoSize);
}
diff --git a/src/com/android/mail/photo/util/FIFEUtil.java b/src/com/android/mail/photo/util/FIFEUtil.java
deleted file mode 100644
index b23182f..0000000
--- a/src/com/android/mail/photo/util/FIFEUtil.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.util;
-
-import android.net.Uri;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Useful FIFE image url manipulation routines.
- */
-public class FIFEUtil {
- private static final Splitter SPLIT_ON_EQUALS = Splitter.on("=").omitEmptyStrings();
-
- private static final Splitter SPLIT_ON_SLASH = Splitter.on("/").omitEmptyStrings();
-
- private static final Joiner JOIN_ON_SLASH = Joiner.on("/");
-
- private static final Pattern FIFE_HOSTED_IMAGE_URL_RE = Pattern.compile("^((http(s)?):)?\\/\\/"
- + "((((lh[3-6]\\.((ggpht)|(googleusercontent)|(google)))"
- + "|([1-4]\\.bp\\.blogspot)|(bp[0-3]\\.blogger))\\.com)"
- + "|(www\\.google\\.com\\/visualsearch\\/lh))\\/");
-
- private static final String EMPTY_STRING = "";
-
- // The ImageUrlOptions path part index for legacy Fife image URLs.
- private static final int LEGACY_URL_PATH_OPTIONS_INDEX = 4;
-
- // Num of path parts a legacy Fife image base URL contains. A base URL
- // contains
- // no ImageUrlOptions nor a filename and is terminated by a slash.
- private static final int LEGACY_BASE_URL_NUM_PATH_PARTS = 4;
- // Number of path parts a legacy Fife image URL contains that has both
- // existing
- // ImageUrlOptions and a filename.
- private static final int LEGACY_WITH_OPTIONS_FILENAME = 5;
-
- // Maximum number of path parts a legacy Fife image URL can contain.
- private static final int LEGACY_URL_MAX_NUM_PATH_PARTS = 6;
-
- // Maximum number of path parts a content Fife image URL can contain.
- private static final int CONTENT_URL_MAX_NUM_PATH_PARTS = 1;
-
- /**
- * Add size options to the given url.
- *
- * @param size the image size
- * @param url the url to apply the options to
- * @param crop if {@code true}, crop the photo to the dimensions
- * @return a {@code Uri} containting the new image url with options.
- */
- public static String setImageUrlSize(int size, String url, boolean crop) {
- return setImageUrlSize(size, url, crop, false);
- }
-
- /**
- * Add size options to the given url.
- *
- * @param size the image size
- * @param url the url to apply the options to
- * @param crop if {@code true}, crop the photo to the dimensions
- * @param includeMetadata if {@code true}, the image returned by the URL will include meta data
- * @return a {@code Uri} containting the new image url with options.
- */
- public static String setImageUrlSize(int size, String url, boolean crop,
- boolean includeMetadata) {
- if (url == null || !isFifeHostedUrl(url)) {
- return url;
- }
-
- final StringBuffer options = new StringBuffer();
- options.append("s").append(size);
- options.append("-d");
- if (crop) {
- options.append("-c");
- }
- if (includeMetadata) {
- options.append("-I");
- }
-
- final Uri uri = setImageUrlOptions(options.toString(), url);
- final String returnUrl = makeUriString(uri);
-
- return returnUrl;
- }
-
- /**
- * Add size options to the given url.
- *
- * @param width the width of the image
- * @param height the height of the image
- * @param url the url to apply the options to
- * @param crop if {@code true}, crop the photo to the dimensions
- * @param includeMetadata if {@code true}, the image returned by the URL will include meta data
- * @return a {@code Uri} containting the new image url with options.
- */
- public static String setImageUrlSize(int width, int height, String url, boolean crop,
- boolean includeMetadata) {
- if (url == null || !isFifeHostedUrl(url)) {
- return url;
- }
-
- final StringBuffer options = new StringBuffer();
- options.append("w").append(width);
- options.append("-h").append(height);
- options.append("-d");
- if (crop) {
- options.append("-c");
- }
- if (includeMetadata) {
- options.append("-I");
- }
-
- final Uri uri = setImageUrlOptions(options.toString(), url);
- final String returnUrl = makeUriString(uri);
-
- return returnUrl;
- }
-
- /**
- * Workaround. When encoding FIFE URL with content image options, the default
- * implementation for Uri.toString() encodes the equals ['='] as "%3D". The
- * FIFE servers choke on this and return a 404.
- */
- private static String makeUriString(Uri uri) {
- final StringBuilder builder = new StringBuilder();
-
- final String scheme = uri.getScheme();
- if (scheme != null) {
- builder.append(scheme).append(':');
- }
-
- final String encodedAuthority = uri.getEncodedAuthority();
- if (encodedAuthority != null) {
- // Even if the authority is "", we still want to append "//".
- builder.append("//").append(encodedAuthority);
- }
-
- final String path = uri.getPath();
- final String encodedPath = Uri.encode(path, "/=");
- if (encodedPath != null) {
- builder.append(encodedPath);
- }
-
- final String encodedQuery = uri.getEncodedQuery();
- if (!TextUtils.isEmpty(encodedQuery)) {
- builder.append('?').append(encodedQuery);
- }
-
- final String encodedFragment = uri.getEncodedFragment();
- if (!TextUtils.isEmpty(encodedFragment)) {
- builder.append('#').append(encodedFragment);
- }
-
- return builder.toString();
- }
-
- /**
- * Add image url options to the given url.
- *
- * @param options the options to apply
- * @param url the url to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- public static Uri setImageUrlOptions(String options, String url) {
- return setImageUriOptions(options, Uri.parse(url));
- }
-
- /**
- * Add image url options to the given url.
- *
- * @param options the options to apply
- * @param uri the uri to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- public static Uri setImageUriOptions(String options, Uri uri) {
- List<String> components = newArrayList(SPLIT_ON_SLASH.split(uri.getPath()));
-
- // Delegate setting ImageUrlOptions based on the Fife image URL format
- // determined by the number of path parts the URL contains.
- int numParts = components.size();
- if (components.size() > 1 && components.get(0).equals("image")) {
- --numParts;
- }
-
- Uri modifiedUri;
- if (numParts >= LEGACY_BASE_URL_NUM_PATH_PARTS
- && numParts <= LEGACY_URL_MAX_NUM_PATH_PARTS) {
- modifiedUri = setLegacyImageUrlOptions(options, uri);
- } else if (numParts == CONTENT_URL_MAX_NUM_PATH_PARTS) {
- modifiedUri = setContentImageUrlOptions(options, uri);
- } else {
- // not a valid URI; don't modify anything
- modifiedUri = uri;
- }
- return modifiedUri;
- }
-
- /**
- * Gets image options from the given url.
- *
- * @param url the url to get the options for
- * @return the image options. or {@link #EMPTY_STRING} if options do not exist.
- */
- public static String getImageUrlOptions(String url) {
- return getImageUriOptions(Uri.parse(url));
- }
-
- /**
- * Gets image options from the given uri.
- *
- * @param uri the uri to get the options for
- * @return the image options. or {@link #EMPTY_STRING} if options do not exist.
- */
- public static String getImageUriOptions(Uri uri) {
- List<String> components = newArrayList(SPLIT_ON_SLASH.split(uri.getPath()));
-
- // Delegate setting ImageUrlOptions based on the Fife image URL format
- // determined by the number of path parts the URL contains.
- int numParts = components.size();
- if (components.size() > 1 && components.get(0).equals("image")) {
- --numParts;
- }
-
- final String options;
- if (numParts >= LEGACY_BASE_URL_NUM_PATH_PARTS
- && numParts <= LEGACY_URL_MAX_NUM_PATH_PARTS) {
- options = getLegacyImageUriOptions(uri);
- } else if (numParts == CONTENT_URL_MAX_NUM_PATH_PARTS) {
- options = getContentImageUriOptions(uri);
- } else {
- // not a valid URI; don't modify anything
- options = EMPTY_STRING;
- }
- return options;
- }
-
- /**
- * Checks if the host is a valid FIFE host.
- *
- * @param url an image url to check
- * @return {@code true} iff the url has a valid FIFE host
- */
- public static boolean isFifeHostedUrl(String url) {
- if (url == null) {
- return false;
- }
-
- Matcher matcher = FIFE_HOSTED_IMAGE_URL_RE.matcher(url);
- return matcher.find();
- }
-
- /**
- * Checks if the host is a valid FIFE host.
- *
- * @param uri an image url to check
- * @return {@code true} iff the url has a valid FIFE host
- */
- public static boolean isFifeHostedUri(Uri uri) {
- return isFifeHostedUrl(uri.toString());
- }
-
- /**
- * Add image url options to the given url.
- *
- * @param options the options to apply
- * @param url the url to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- private static Uri setLegacyImageUrlOptions(String options, Uri url) {
- String path = url.getPath();
- List<String> components = newArrayList(SPLIT_ON_SLASH.split(path));
- boolean hasImagePrefix = false;
-
- if (components.size() > 0 && components.get(0).equals("image")) {
- components.remove(0);
- hasImagePrefix = true;
- }
-
- int numParts = components.size();
- boolean isPathSlashTerminated = path.endsWith("/");
- boolean containsFilenameNoOptions =
- !isPathSlashTerminated && numParts == LEGACY_WITH_OPTIONS_FILENAME;
- boolean isBaseUrlFormat = numParts == LEGACY_BASE_URL_NUM_PATH_PARTS;
-
- // Make room for the options in the path components if no options previously existed.
- if (containsFilenameNoOptions) {
- components.add(components.get(LEGACY_URL_PATH_OPTIONS_INDEX));
- }
-
- if (isBaseUrlFormat) {
- components.add(options);
- } else {
- components.set(LEGACY_URL_PATH_OPTIONS_INDEX, options);
- }
-
- // Put back image component if was there before.
- if (hasImagePrefix) {
- components.add(0, "image");
- }
-
- // Terminate the new path with a slash if required.
- if (isPathSlashTerminated) {
- components.add("");
- }
-
- return url.buildUpon().path("/" + JOIN_ON_SLASH.join(components)).build();
- }
-
- /**
- * Add image url options to the given url.
- *
- * @param options the options to apply
- * @param url the url to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- private static Uri setContentImageUrlOptions(String options, Uri url) {
- List<String> splitPath = newArrayList(SPLIT_ON_EQUALS.split(url.getPath()));
- String path = splitPath.get(0) + "=" + options;
-
- return url.buildUpon().path(path).build();
- }
-
- /**
- * Gets image options from the given URI.
- *
- * @param uri the URI to get the options for
- * @return the image options. or {@link #EMPTY_STRING} if options do not exist.
- */
- private static String getLegacyImageUriOptions(Uri uri) {
- String path = uri.getPath();
- List<String> components = newArrayList(SPLIT_ON_SLASH.split(path));
-
- if (components.size() > 0 && components.get(0).equals("image")) {
- components.remove(0);
- }
-
- int numParts = components.size();
- boolean isPathSlashTerminated = path.endsWith("/");
- boolean containsFilenameNoOptions =
- !isPathSlashTerminated && numParts == LEGACY_WITH_OPTIONS_FILENAME;
- boolean isBaseUrlFormat = numParts == LEGACY_BASE_URL_NUM_PATH_PARTS;
-
- // No options in the URI
- if (containsFilenameNoOptions) {
- return EMPTY_STRING;
- }
-
- if (!isBaseUrlFormat) {
- return components.get(LEGACY_URL_PATH_OPTIONS_INDEX);
- }
-
- return EMPTY_STRING;
- }
-
- /**
- * Gets image options from the given URI.
- *
- * @param uri the URI to get the options for
- * @return the image options. or {@link #EMPTY_STRING} if options do not exist.
- */
- private static String getContentImageUriOptions(Uri uri) {
- List<String> splitPath = newArrayList(SPLIT_ON_EQUALS.split(uri.getPath()));
- return (splitPath.size() > 1) ? splitPath.get(1) : EMPTY_STRING;
- }
-
- // Private. Just a class full of static functions.
- private FIFEUtil() {
- }
-
-
-
-
- /*
- * The code below has been shamelessly copied from guava to avoid bringing in it's 700+K
- * library for just a few lines of code. This is <em>NOT</em> meant to provide a fully
- * functional replacement. It only provides enough functionality to modify FIFE URLs.
- */
-
- /**
- * Creates a <i>mutable</i> {@code ArrayList} instance containing the given
- * elements.
- */
- private static <E> ArrayList<E> newArrayList(Iterable<? extends E> elements) {
- // Let ArrayList's sizing logic work, if possible
- Iterator<? extends E> iterator = elements.iterator();
- ArrayList<E> list = new ArrayList<E>();
- while (iterator.hasNext()) {
- list.add(iterator.next());
- }
- return list;
- }
-
- /**
- * Joins pieces of text with a separator.
- */
- private static class Joiner {
- public static Joiner on(String separator) {
- return new Joiner(separator);
- }
-
- private final String separator;
-
- private Joiner(String separator) {
- this.separator = separator;
- }
-
- /**
- * Appends each of part, using the configured separator between each.
- */
- public final StringBuilder appendTo(StringBuilder builder, Iterable<?> parts) {
- Iterator<?> iterator = parts.iterator();
- if (iterator.hasNext()) {
- builder.append(toString(iterator.next()));
- while (iterator.hasNext()) {
- builder.append(separator);
- builder.append(toString(iterator.next()));
- }
- }
- return builder;
- }
-
- /**
- * Returns a string containing the string representation of each of
- * {@code parts}, using the previously configured separator between
- * each.
- */
- public final String join(Iterable<?> parts) {
- return appendTo(new StringBuilder(), parts).toString();
- }
-
- CharSequence toString(Object part) {
- return (part instanceof CharSequence) ? (CharSequence) part : part.toString();
- }
- }
-
- /**
- * Divides strings into substrings, by recognizing a separator (a.k.a. "delimiter").
- */
- static class Splitter {
- private final boolean omitEmptyStrings;
- private final Strategy strategy;
-
- private Splitter(Strategy strategy) {
- this(strategy, false);
- }
-
- private Splitter(Strategy strategy, boolean omitEmptyStrings) {
- this.strategy = strategy;
- this.omitEmptyStrings = omitEmptyStrings;
- }
-
- public static Splitter on(final String separator) {
- if (separator == null || separator.length() == 0) {
- throw new IllegalArgumentException("separator may not be empty or null");
- }
-
- return new Splitter(new Strategy() {
- @Override
- public SplittingIterator iterator(Splitter splitter, CharSequence toSplit) {
- return new SplittingIterator(splitter, toSplit) {
- @Override
- public int separatorStart(int start) {
- int delimeterLength = separator.length();
-
- positions: for (
- int p = start, last = toSplit.length() - delimeterLength;
- p <= last;
- p++) {
- for (int i = 0; i < delimeterLength; i++) {
- if (toSplit.charAt(i + p) != separator.charAt(i)) {
- continue positions;
- }
- }
- return p;
- }
- return -1;
- }
-
- @Override
- public int separatorEnd(int separatorPosition) {
- return separatorPosition + separator.length();
- }
- };
- }
- });
- }
-
- public Splitter omitEmptyStrings() {
- return new Splitter(strategy, true);
- }
-
- public Iterable<String> split(final CharSequence sequence) {
- return new Iterable<String>() {
- @Override
- public Iterator<String> iterator() {
- return strategy.iterator(Splitter.this, sequence);
- }
- };
- }
-
- private interface Strategy {
- Iterator<String> iterator(Splitter splitter, CharSequence toSplit);
- }
-
- private abstract static class SplittingIterator extends AbstractIterator<String> {
- final CharSequence toSplit;
- final boolean omitEmptyStrings;
-
- abstract int separatorStart(int start);
-
- abstract int separatorEnd(int separatorPosition);
-
- int offset = 0;
-
- protected SplittingIterator(Splitter splitter, CharSequence toSplit) {
- this.omitEmptyStrings = splitter.omitEmptyStrings;
- this.toSplit = toSplit;
- }
-
- @Override
- protected String computeNext() {
- while (offset != -1) {
- int start = offset;
- int end;
-
- int separatorPosition = separatorStart(offset);
- if (separatorPosition == -1) {
- end = toSplit.length();
- offset = -1;
- } else {
- end = separatorPosition;
- offset = separatorEnd(separatorPosition);
- }
-
- if (omitEmptyStrings && start == end) {
- continue;
- }
-
- return toSplit.subSequence(start, end).toString();
- }
- return endOfData();
- }
- }
-
- private static abstract class AbstractIterator<T> implements Iterator<T> {
- State state = State.NOT_READY;
-
- enum State {
- READY, NOT_READY, DONE, FAILED,
- }
-
- T next;
-
- protected abstract T computeNext();
-
- protected final T endOfData() {
- state = State.DONE;
- return null;
- }
-
- @Override
- public final boolean hasNext() {
- if (state == State.FAILED) {
- throw new IllegalStateException();
- }
-
- switch (state) {
- case DONE:
- return false;
- case READY:
- return true;
- default:
- }
- return tryToComputeNext();
- }
-
- boolean tryToComputeNext() {
- state = State.FAILED; // temporary pessimism
- next = computeNext();
- if (state != State.DONE) {
- state = State.READY;
- return true;
- }
- return false;
- }
-
- @Override
- public final T next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- state = State.NOT_READY;
- return next;
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
- }
- }
-}
diff --git a/src/com/android/mail/photo/util/ImageCache.java b/src/com/android/mail/photo/util/ImageCache.java
deleted file mode 100644
index 31194c4..0000000
--- a/src/com/android/mail/photo/util/ImageCache.java
+++ /dev/null
@@ -1,1274 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.util;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Looper;
-import android.os.Message;
-import android.support.v4.util.LruCache;
-
-import com.android.mail.R;
-import com.android.mail.photo.content.ImageRequest;
-import com.android.mail.photo.content.LocalImageRequest;
-import com.android.mail.photo.content.MediaImageRequest;
-
-import java.lang.ref.SoftReference;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Asynchronously loads images and maintains a cache of those.
- */
-public class ImageCache implements Callback {
-
- /**
- * The listener interface notifying of avatar changes.
- */
- public interface OnAvatarChangeListener {
-
- /**
- * Invoked if a new avatar has been loaded for the specified Gaia ID.
- */
- void onAvatarChanged(String gaiaId);
- }
-
- /**
- * The listener interface notifying of media image changes.
- */
- public interface OnMediaImageChangeListener {
-
- /**
- * Invoked if a new media image has been loaded for the specified URL.
- */
- void onMediaImageChanged(String url);
- }
-
- /**
- * The listener interface notifying of image changes.
- */
- public interface OnRemoteImageChangeListener {
-
- /**
- * Invoked if a new media image has been loaded for the specified URL.
- */
- void onRemoteImageChanged(ImageRequest request, Bitmap bitmap);
- }
-
- /**
- * The listener interface notifying of remote image changes.
- */
- public interface OnRemoteDrawableChangeListener extends OnRemoteImageChangeListener {
-
- /**
- * Invoked if a new image has been loaded for the specified URL.
- */
- void onRemoteImageChanged(ImageRequest request, Drawable drawable);
- }
-
- /**
- * The listener interface notifying a listener that an image load request has been completed.
- */
- public interface OnImageRequestCompleteListener {
-
- /**
- * Invoked when a request is complete.
- */
- void onImageRequestComplete(ImageRequest request);
- }
-
- /**
- * The callback interface that must be implemented by the views requesting images.
- */
- public interface ImageConsumer {
-
- /**
- * @param bitmap The bitmap
- * @param loading The flag indicating if the image is still loading (if
- * true, bitmap will be null).
- */
- void setBitmap(Bitmap bitmap, boolean loading);
- }
-
- /**
- * The callback interface that can optionally be implemented by the views
- * requesting images if they want to support animated drawables.
- */
- public interface DrawableConsumer extends ImageConsumer {
-
- /**
- * @param drawable The image
- * @param loading The flag indicating if the image is still loading (if
- * true, bitmap will be null).
- */
- void setDrawable(Drawable drawable, boolean loading);
- }
-
- // Logging.
- static final String TAG = "ImageCache";
-
- private static final String LOADER_THREAD_NAME = "ImageCache";
-
- /**
- * Type of message sent by the UI thread to itself to indicate that some photos
- * need to be loaded.
- */
- private static final int MESSAGE_REQUEST_LOADING = 1;
-
- /**
- * Type of message sent by the loader thread to indicate that some photos have
- * been loaded.
- */
- private static final int MESSAGE_IMAGES_LOADED = 2;
-
- /**
- * Type of message sent to indicate that an avatar has changed.
- */
- private static final int MESSAGE_AVATAR_CHANGED = 3;
-
- /**
- * Type of message sent to indicate that a media image has changed.
- */
- private static final int MESSAGE_MEDIA_IMAGE_CHANGED = 4;
-
- /**
- * Type of message sent to indicate that an image has changed.
- */
- private static final int MESSAGE_REMOTE_IMAGE_CHANGED = 5;
-
- private static final byte[] EMPTY_ARRAY = new byte[0];
-
- /**
- * Maintains the state of a particular photo.
- */
- private static class ImageHolder {
- final byte[] bytes;
- final boolean complete;
-
- volatile boolean fresh;
-
- /**
- * Either {@link Bitmap} or {@link Drawable}.
- */
- Object image;
- SoftReference<Object> imageRef;
-
-
- public ImageHolder(byte[] bytes, boolean complete) {
- this.bytes = bytes;
- this.fresh = true;
- this.complete = complete;
- }
- }
-
- private static class MediaImageChangeNotification {
- MediaImageRequest request;
- byte[] imageBytes;
- }
-
- private static class RemoteImageChangeNotification {
- ImageRequest request;
- byte[] imageBytes;
- }
-
- private static final float ESTIMATED_BYTES_PER_PIXEL = 0.3f;
-
- private static int sTinyAvatarEstimatedSize;
- private static int sSmallAvatarEstimatedSize;
- private static int sMediumAvatarEstimatedSize;
-
- private static boolean sUseSoftReferences;
-
- private final Context mContext;
-
- private static HashSet<OnAvatarChangeListener> mAvatarListeners =
- new HashSet<OnAvatarChangeListener>();
-
- private static HashSet<OnMediaImageChangeListener> mMediaImageListeners =
- new HashSet<OnMediaImageChangeListener>();
-
- private static HashSet<OnRemoteImageChangeListener> mRemoteImageListeners =
- new HashSet<OnRemoteImageChangeListener>();
-
- private static HashSet<OnImageRequestCompleteListener> mRequestCompleteListeners =
- new HashSet<OnImageRequestCompleteListener>();
-
- /**
- * An LRU cache for image holders. The cache contains bytes for images just
- * as they come from the database. Each holder has a soft reference to the
- * actual image.
- */
- private final LruCache<ImageRequest, ImageHolder> mImageHolderCache;
-
- /**
- * Cache size threshold at which images will not be preloaded.
- */
- private final int mImageHolderCacheRedZoneBytes;
-
- /**
- * Level 2 LRU cache for images. This is a smaller cache that holds
- * the most recently used images to save time on decoding
- * them from bytes (the bytes are stored in {@link #mImageHolderCache}.
- */
- private final LruCache<ImageRequest, Object> mImageCache;
-
- /**
- * A map from {@link ImageConsumer} to the corresponding {@link ImageRequest}. Please
- * note that this request may change before the photo loading request is
- * started.
- */
- private final ConcurrentHashMap<ImageConsumer, ImageRequest> mPendingRequests =
- new ConcurrentHashMap<ImageConsumer, ImageRequest>();
-
- /**
- * Handler for messages sent to the UI thread.
- */
- private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), this);
-
-// /**
-// * Thread responsible for loading photos from the database. Created upon
-// * the first request.
-// */
-// private LoaderThread mLoaderThread;
-
- /**
- * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
- */
- private boolean mLoadingRequested;
-
- /**
- * Flag indicating if the image loading is paused.
- */
- private boolean mPaused;
-
- private static ImageCache sInstance;
-
- public static synchronized ImageCache getInstance(Context context) {
-
- // We can use one global instance provided that we bind to the
- // application context instead of the context that is passed in.
- // Otherwise this static instance would retain the supplied context and
- // cause a leak.
- if (sInstance == null) {
- sInstance = new ImageCache(context.getApplicationContext());
- }
- return sInstance;
- }
-
- private ImageCache(Context context) {
- mContext = context;
-
- Resources resources = context.getApplicationContext().getResources();
- mImageCache = new LruCache<ImageRequest, Object>(
- resources.getInteger(R.integer.config_image_cache_max_bitmaps));
- int maxBytes = resources.getInteger(R.integer.config_image_cache_max_bytes);
- mImageHolderCache = new LruCache<ImageRequest, ImageHolder>(maxBytes) {
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected int sizeOf(ImageRequest request, ImageHolder value) {
- return value.bytes != null ? value.bytes.length : 0;
- }
- };
-
- mImageHolderCacheRedZoneBytes = (int) (maxBytes * 0.9);
-
- if (sTinyAvatarEstimatedSize == 0) {
-// sTinyAvatarEstimatedSize = (int) (ESTIMATED_BYTES_PER_PIXEL
-// * EsAvatarData.getTinyAvatarSize(context)
-// * EsAvatarData.getTinyAvatarSize(context));
-// sSmallAvatarEstimatedSize = (int) (ESTIMATED_BYTES_PER_PIXEL
-// * EsAvatarData.getSmallAvatarSize(context)
-// * EsAvatarData.getSmallAvatarSize(context));
-// sMediumAvatarEstimatedSize = (int) (ESTIMATED_BYTES_PER_PIXEL
-// * EsAvatarData.getMediumAvatarSize(context)
-// * EsAvatarData.getMediumAvatarSize(context));
-
- sUseSoftReferences = Build.VERSION.SDK_INT >= 11;
- }
- }
-
-// /**
-// * Returns an estimate of the avatar size in bytes.
-// */
-// private int getEstimatedSizeInBytes(AvatarRequest request) {
-// switch (request.getSize()) {
-// case AvatarRequest.TINY: return sTinyAvatarEstimatedSize;
-// case AvatarRequest.SMALL: return sSmallAvatarEstimatedSize;
-// case AvatarRequest.MEDIUM: return sMediumAvatarEstimatedSize;
-// }
-// return 0;
-// }
-
- /**
- * Clears cache.
- */
- public void clear() {
- mImageHolderCache.evictAll();
- mImageCache.evictAll();
- mPendingRequests.clear();
- }
-
-// /**
-// * Starts preloading photos in the background.
-// */
-// public void preloadAvatarsInBackground(List<AvatarRequest> requests) {
-// ensureLoaderThread();
-//
-// boolean preloadingNeeded = touchRequestedEntries(requests);
-// int totalTinyAvatarSize = touchTinyAvatars();
-//
-// if (!preloadingNeeded) {
-// return;
-// }
-//
-// requests = trimCache(requests, totalTinyAvatarSize);
-//
-// mLoaderThread.startPreloading(requests);
-// }
-
-// /**
-// * Adjust the LRU order of the requested images that are already cached to prevent them
-// * from being evicted by preloading.
-// */
-// private boolean touchRequestedEntries(List<AvatarRequest> requests) {
-// boolean cacheMissed = false;
-// for (int i = requests.size() - 1; i >= 0; i--) {
-// AvatarRequest request = requests.get(i);
-// ImageHolder holder = mImageHolderCache.get(request);
-// if (holder != null) {
-// mImageHolderCache.put(request, holder);
-// } else {
-// cacheMissed = true;
-// }
-// }
-//
-// return cacheMissed;
-// }
-
-// /**
-// * Moves tiny avatars to the top of the LRU order to try and keep them from being evicted.
-// * Caching tiny avatars is highly cost-effective.
-// *
-// * @return The total size of all tiny avatars in cache.
-// */
-// private int touchTinyAvatars() {
-// int totalSize = 0;
-//
-// Iterator<Entry<ImageRequest, ImageHolder>> iterator =
-// mImageHolderCache.snapshot().entrySet().iterator();
-// while (iterator.hasNext()) {
-// Entry<ImageRequest, ImageHolder> entry = iterator.next();
-// ImageRequest request = entry.getKey();
-// if ((request instanceof AvatarRequest)
-// && ((AvatarRequest) request).getSize() == AvatarRequest.TINY) {
-// ImageHolder holder = entry.getValue();
-// if (holder.bytes != null) {
-// totalSize += holder.bytes.length;
-// }
-//
-// mImageHolderCache.put(request, holder);
-// }
-// }
-//
-// return totalSize;
-// }
-
-// /**
-// * Reduces the size of cache before preloading.
-// */
-// private List<AvatarRequest> trimCache(List<AvatarRequest> requests, int totalTinyAvatarSize) {
-// int preferredCacheSize = mImageHolderCacheRedZoneBytes;
-// int estimatedMemoryUse = totalTinyAvatarSize;
-// for (int i = 0; i < requests.size(); i++) {
-// if (estimatedMemoryUse >= mImageHolderCacheRedZoneBytes) {
-// trimCache(preferredCacheSize);
-// return requests.subList(0, i);
-// }
-//
-// AvatarRequest request = requests.get(i);
-// ImageHolder holder = mImageHolderCache.get(request);
-// if (holder != null && holder.bytes != null) {
-// estimatedMemoryUse += holder.bytes.length;
-// } else {
-// int bytes = getEstimatedSizeInBytes(request);
-// preferredCacheSize -= bytes;
-// estimatedMemoryUse += bytes;
-// }
-// }
-//
-// trimCache(preferredCacheSize);
-// return requests;
-// }
-
- /**
- * Shrinks cache to the desired size.
- */
- private void trimCache(int size) {
- Iterator<Entry<ImageRequest, ImageHolder>> iterator =
- mImageHolderCache.snapshot().entrySet().iterator();
- while (mImageHolderCache.size() > size && iterator.hasNext()) {
- mImageHolderCache.remove(iterator.next().getKey());
- }
- }
-
- /**
- * Evicts avatars that were requested but never loaded. This will force them to be
- * requested again if needed.
- */
- public void refresh() {
- Iterator<ImageHolder> iterator = mImageHolderCache.snapshot().values().iterator();
- while (iterator.hasNext()) {
- ImageHolder holder = iterator.next();
- if (!holder.complete) {
- holder.fresh = false;
- }
- }
- }
-
- /**
- * Requests asynchronous photo loading for the specified request.
- *
- * @param consumer Image consumer
- * @param request The combination of URL, type and size.
- */
- public void loadImage(ImageConsumer consumer, ImageRequest request) {
- loadImage(consumer, request, true);
- }
-
- /**
- * Requests an asynchronous refresh of the image for the specified request.
- *
- * @param consumer Image consumer
- * @param request The combination of URL, type and size.
- */
- public void refreshImage(ImageConsumer consumer, ImageRequest request) {
- loadImage(consumer, request, false);
- }
-
- /**
- * Evicts all local images from the cache.
- */
- public void evictAllLocalImages() {
- Set<ImageRequest> iterator = mImageHolderCache.snapshot().keySet();
- for (ImageRequest request : iterator) {
- if (request instanceof LocalImageRequest) {
- mImageCache.remove(request);
- mImageHolderCache.remove(request);
- }
- }
- }
-
- private void loadImage(ImageConsumer consumer, ImageRequest request,
- boolean clearIfNotCached) {
- if (request.isEmpty()) {
- // No photo is needed
- consumer.setBitmap(null, false);
- notifyRequestComplete(request);
- mPendingRequests.remove(consumer);
- } else {
- boolean loaded = loadCachedImage(consumer, request, clearIfNotCached);
- if (loaded) {
- mPendingRequests.remove(consumer);
- } else {
- mPendingRequests.put(consumer, request);
- if (!mPaused) {
- // Send a request to start loading photos
- requestLoading();
- }
- }
- }
- }
-
- /**
- * Registers an avatar change listener.
- */
- public void registerAvatarChangeListener(OnAvatarChangeListener listener) {
- mAvatarListeners.add(listener);
- }
-
- /**
- * Unregisters an avatar change listener.
- */
- public void unregisterAvatarChangeListener(OnAvatarChangeListener listener) {
- mAvatarListeners.remove(listener);
- }
-
-// /**
-// * Sends a notification to all registered listeners that the avatar for the
-// * specified Gaia ID has changed.
-// */
-// public void notifyAvatarChange(String gaiaId) {
-// if (gaiaId == null) {
-// return;
-// }
-//
-// ensureLoaderThread();
-// mLoaderThread.notifyAvatarChange(gaiaId);
-// }
-
- /**
- * Registers a media image change listener.
- */
- public void registerMediaImageChangeListener(OnMediaImageChangeListener listener) {
- mMediaImageListeners.add(listener);
- }
-
- /**
- * Unregisters a media image change listener.
- */
- public void unregisterMediaImageChangeListener(OnMediaImageChangeListener listener) {
- mMediaImageListeners.remove(listener);
- }
-
-// /**
-// * Sends a notification to all registered listeners that the media image for the
-// * specified URL has changed.
-// */
-// public void notifyMediaImageChange(MediaImageRequest request, byte[] imageBytes) {
-// ensureLoaderThread();
-// MediaImageChangeNotification notification = new MediaImageChangeNotification();
-// notification.request = request;
-// notification.imageBytes = imageBytes;
-// mLoaderThread.notifyMediaImageChange(notification);
-// }
-
- /**
- * Registers a remote image change listener.
- */
- public void registerRemoteImageChangeListener(OnRemoteImageChangeListener listener) {
- mRemoteImageListeners.add(listener);
- }
-
- /**
- * Unregisters a remote image change listener.
- */
- public void unregisterRemoteImageChangeListener(OnRemoteImageChangeListener listener) {
- mRemoteImageListeners.remove(listener);
- }
-
-// /**
-// * Sends a notification to all registered listeners that the remote image for the
-// * specified URL has changed.
-// */
-// public void notifyRemoteImageChange(ImageRequest request, byte[] imageBytes) {
-// ensureLoaderThread();
-// RemoteImageChangeNotification notification = new RemoteImageChangeNotification();
-// notification.request = request;
-// notification.imageBytes = imageBytes;
-// mLoaderThread.notifyRemoteImageChange(notification);
-// }
-
- /**
- * Registers an image request completion listener.
- */
- public void registerRequestCompleteListener(OnImageRequestCompleteListener listener) {
- mRequestCompleteListeners.add(listener);
- }
-
- /**
- * Unregisters an image request completion listener.
- */
- public void unregisterRequestCompleteListener(OnImageRequestCompleteListener listener) {
- mRequestCompleteListeners.remove(listener);
- }
-
- private void notifyRequestComplete(ImageRequest request) {
- for (OnImageRequestCompleteListener listener : mRequestCompleteListeners) {
- listener.onImageRequestComplete(request);
- }
- }
-
- /**
- * Checks if the photo is present in cache. If so, sets the photo on the view.
- *
- * @return false if the photo needs to be (re)loaded from the provider.
- */
- private boolean loadCachedImage(ImageConsumer consumer, ImageRequest request,
- boolean clearIfNotCached) {
- ImageHolder holder = mImageHolderCache.get(request);
- if (holder == null) {
- if (clearIfNotCached) {
- // The bitmap has not been loaded - should display the placeholder image.
- consumer.setBitmap(null, true);
- }
- return false;
- }
-
- // Put this holder on top of the LRU list
- mImageHolderCache.put(request, holder);
-
- if (holder.bytes == null) {
- if (holder.complete) {
- consumer.setBitmap(null, false);
- notifyRequestComplete(request);
- } else {
- // The bitmap has not been loaded from server - should display a placeholder.
- consumer.setBitmap(null, true);
- }
- return holder.fresh;
- }
-
- // Optionally decode bytes into a bitmap.
- inflateImage(request, holder);
-
- Object image = holder.image;
- if (image instanceof Bitmap) {
- consumer.setBitmap((Bitmap) image, false);
- } else if (consumer instanceof DrawableConsumer) {
- ((DrawableConsumer)consumer).setDrawable((Drawable) image, false);
- } else if (image instanceof GifDrawable) {
- consumer.setBitmap(((GifDrawable)image).getFirstFrame(), false);
- } else if (image != null) {
- throw new UnsupportedOperationException("Cannot handle drawables of type "
- + image.getClass());
- }
-
- notifyRequestComplete(request);
-
- // Put the bitmap in the LRU cache
- if (image != null && holder.fresh) {
- mImageCache.put(request, image);
- }
-
- // Soften the reference
- holder.image = null;
-
- return holder.fresh;
- }
-
-// /**
-// * Returns a photo from cache or null if it is not cached. Does not trigger a load.
-// * Returns an empty byte array if the photo is known to be missing.
-// */
-// public byte[] getCachedAvatar(AvatarRequest request) {
-// ImageHolder holder = mImageHolderCache.get(request);
-// if (holder == null || !holder.fresh) {
-// return null;
-// }
-//
-// if (holder.bytes == null) {
-// return EMPTY_ARRAY;
-// }
-//
-// return holder.bytes;
-// }
-
- /**
- * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
- * bitmap is held either by {@link #mImageCache} or by a soft reference in
- * the holder, it will not be necessary to decode the bitmap.
- */
- private void inflateImage(ImageRequest request, ImageHolder holder) {
- if (holder.image != null) {
- return;
- }
-
- byte[] bytes = holder.bytes;
- if (bytes == null || bytes.length == 0) {
- return;
- }
-
- holder.image = mImageCache.get(request);
- if (holder.image != null) {
- return;
- }
-
- // Check the soft reference. If will be retained if the bitmap is also
- // in the LRU cache, so we don't need to check the LRU cache explicitly.
- if (holder.imageRef != null) {
- holder.image = holder.imageRef.get();
- if (holder.image != null) {
- return;
- }
- }
-
- try {
- holder.image = ImageUtils.decodeMedia(bytes);
- if (holder.image == null) {
- holder.imageRef = null;
- } else if (sUseSoftReferences) {
- holder.imageRef = new SoftReference<Object>(holder.image);
- }
- } catch (OutOfMemoryError e) {
- // Do nothing - the photo will appear to be missing
- }
- }
-
- public void pause() {
- mPaused = true;
- }
-
- public void resume() {
- mPaused = false;
- if (!mPendingRequests.isEmpty()) {
- requestLoading();
- }
- }
-
- /**
- * Sends a message to this thread itself to start loading images. If the current
- * view contains multiple image views, all of those image views will get a chance
- * to request their respective photos before any of those requests are executed.
- * This allows us to load images in bulk.
- */
- private void requestLoading() {
- if (!mLoadingRequested) {
- mLoadingRequested = true;
- mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
- }
- }
-
- /**
- * Processes requests on the main thread.
- */
- @Override
- public boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MESSAGE_REQUEST_LOADING: {
- mLoadingRequested = false;
- if (!mPaused) {
-// ensureLoaderThread();
-// mLoaderThread.requestLoading();
- }
- return true;
- }
-
- case MESSAGE_IMAGES_LOADED: {
- if (!mPaused) {
- processLoadedImages();
- }
- return true;
- }
-
-// case MESSAGE_AVATAR_CHANGED: {
-// String gaiaId = (String) msg.obj;
-//
-// evictImage(new AvatarRequest(gaiaId, AvatarRequest.TINY));
-// evictImage(new AvatarRequest(gaiaId, AvatarRequest.SMALL));
-// evictImage(new AvatarRequest(gaiaId, AvatarRequest.MEDIUM));
-//
-// for (OnAvatarChangeListener listener : mAvatarListeners) {
-// listener.onAvatarChanged(gaiaId);
-// }
-// return true;
-// }
-
- case MESSAGE_MEDIA_IMAGE_CHANGED: {
- MediaImageChangeNotification notification = (MediaImageChangeNotification) msg.obj;
- String url = notification.request.getUrl();
- for (ImageRequest request : mImageHolderCache.snapshot().keySet()) {
- if (!request.equals(notification.request)
- && (request instanceof MediaImageRequest)
- && url.equals(((MediaImageRequest) request).getUrl())) {
- evictImage(request);
- }
- }
-
- for (OnMediaImageChangeListener listener : mMediaImageListeners) {
- listener.onMediaImageChanged(url);
- }
- return true;
- }
-
- case MESSAGE_REMOTE_IMAGE_CHANGED: {
- final RemoteImageChangeNotification notification =
- (RemoteImageChangeNotification) msg.obj;
- final ImageRequest notificationRequest = notification.request;
- final ImageHolder holder = mImageHolderCache.get(notificationRequest);
- final Object image = (holder != null) ? holder.image : null;
-
- for (OnRemoteImageChangeListener listener : mRemoteImageListeners) {
- if (image instanceof Bitmap || image == null) {
- listener.onRemoteImageChanged(notificationRequest, (Bitmap) image);
- } else if (image instanceof GifDrawable) {
- GifDrawable drawable = (GifDrawable) image;
- if (listener instanceof OnRemoteDrawableChangeListener) {
- ((OnRemoteDrawableChangeListener) listener).onRemoteImageChanged(
- notificationRequest, drawable);
- } else {
- listener.onRemoteImageChanged(
- notificationRequest, drawable.getFirstFrame());
- }
- } else {
- throw new UnsupportedOperationException("Unsupported remote image type "
- + image.getClass());
- }
- }
- return true;
- }
- }
- return false;
- }
-
- private void evictImage(ImageRequest request) {
- mImageCache.remove(request);
- ImageHolder holder = mImageHolderCache.get(request);
- if (holder != null) {
- holder.fresh = false;
- }
- }
-
-// public void ensureLoaderThread() {
-// if (mLoaderThread == null) {
-// mLoaderThread = new LoaderThread(mContext.getContentResolver());
-// mLoaderThread.start();
-// }
-// }
-
- /**
- * Goes over pending loading requests and displays loaded photos. If some of the
- * photos still haven't been loaded, sends another request for image loading.
- */
- private void processLoadedImages() {
- Iterator<ImageConsumer> iterator = mPendingRequests.keySet().iterator();
- while (iterator.hasNext()) {
- ImageConsumer consumer = iterator.next();
- ImageRequest request = mPendingRequests.get(consumer);
- boolean loaded = loadCachedImage(consumer, request, false);
- if (loaded) {
- iterator.remove();
- }
- }
-
- softenCache();
-
- if (!mPendingRequests.isEmpty()) {
- requestLoading();
- }
- }
-
- /**
- * Removes strong references to loaded images to allow them to be garbage collected
- * if needed. Some of the images will still be retained by {@link #mImageCache}.
- */
- private void softenCache() {
- for (ImageHolder holder : mImageHolderCache.snapshot().values()) {
- holder.image = null;
- }
- }
-
- /**
- * Stores the supplied image in cache.
- */
- private void deliverImage(ImageRequest request, byte[] bytes, boolean available,
- boolean preloading) {
- ImageHolder holder = new ImageHolder(bytes, available);
- holder.fresh = true;
-
- // Unless this image is being preloaded, decode it right away while
- // we are still on the background thread.
- if (available && !preloading) {
- inflateImage(request, holder);
- }
-
- mImageHolderCache.put(request, holder);
- }
-
- /**
- * Populates an array of photo IDs that need to be loaded.
- */
- private void obtainRequestsToLoad(HashSet<ImageRequest> requests) {
- requests.clear();
-
- /*
- * Since the call is made from the loader thread, the map could be
- * changing during the iteration. That's not really a problem:
- * ConcurrentHashMap will allow those changes to happen without throwing
- * exceptions. Since we may miss some requests in the situation of
- * concurrent change, we will need to check the map again once loading
- * is complete.
- */
- Iterator<ImageRequest> iterator = mPendingRequests.values().iterator();
- while (iterator.hasNext()) {
- ImageRequest key = iterator.next();
- ImageHolder holder = mImageHolderCache.get(key);
- if (holder == null || !holder.fresh) {
- requests.add(key);
- }
- }
- }
-
-// /**
-// * The thread that performs loading of photos from the database.
-// */
-// private class LoaderThread extends HandlerThread implements Callback {
-// private static final int MESSAGE_PRELOAD_AVATARS = 0;
-// private static final int MESSAGE_CONTINUE_PRELOAD = 1;
-// private static final int MESSAGE_LOAD_IMAGES = 2;
-// private static final int MESSAGE_NOTIFY_AVATAR_CHANGE = 3;
-// private static final int MESSAGE_NOTIFY_MEDIA_IMAGE_CHANGE = 4;
-// private static final int MESSAGE_NOTIFY_REMOTE_IMAGE_CHANGE = 5;
-//
-// /**
-// * A pause between preload batches that yields to the UI thread.
-// */
-// private static final int AVATAR_PRELOAD_DELAY = 50;
-//
-// /**
-// * Number of photos to preload per batch.
-// */
-// private static final int PRELOAD_BATCH = 25;
-//
-// private final HashSet<ImageRequest> mRequests = new HashSet<ImageRequest>();
-//// private List<AvatarRequest> mPreloadRequests = new ArrayList<AvatarRequest>();
-//
-// private Handler mLoaderThreadHandler;
-//
-// private static final int PRELOAD_STATUS_NOT_STARTED = 0;
-// private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
-// private static final int PRELOAD_STATUS_DONE = 2;
-//
-// private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
-//
-// public LoaderThread(ContentResolver resolver) {
-// super(LOADER_THREAD_NAME);
-// }
-//
-// public void ensureHandler() {
-// if (mLoaderThreadHandler == null) {
-// mLoaderThreadHandler = new Handler(getLooper(), this);
-// }
-// }
-//
-//// /**
-//// * Kicks off preloading of the photos on the background thread.
-//// * Preloading will happen after a delay: we want to yield to the UI thread
-//// * as much as possible.
-//// * <p>
-//// * If preloading is already complete, does nothing.
-//// */
-//// public void startPreloading(List<AvatarRequest> requests) {
-//// ensureHandler();
-////
-//// mLoaderThreadHandler.sendMessage(mLoaderThreadHandler.obtainMessage(
-//// MESSAGE_PRELOAD_AVATARS, requests));
-//// }
-//
-// /**
-// * Kicks off preloading of the next batch of photos on the background thread.
-// * Preloading will happen after a delay: we want to yield to the UI thread
-// * as much as possible.
-// * <p>
-// * If preloading is already complete, does nothing.
-// */
-// public void continuePreloading() {
-// if (mPreloadStatus == PRELOAD_STATUS_DONE) {
-// return;
-// }
-//
-// ensureHandler();
-// if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_IMAGES)) {
-// return;
-// }
-//
-// mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_CONTINUE_PRELOAD,
-// AVATAR_PRELOAD_DELAY);
-// }
-//
-// /**
-// * Sends a message to this thread to load requested photos.
-// */
-// public void requestLoading() {
-// ensureHandler();
-// mLoaderThreadHandler.removeMessages(MESSAGE_CONTINUE_PRELOAD);
-// mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_IMAGES);
-// }
-//
-// /**
-// * Channels a change notification event through the loader thread to ensure
-// * proper concurrency.
-// */
-// public void notifyAvatarChange(String gaiaId) {
-// ensureHandler();
-// Message msg = mLoaderThreadHandler.obtainMessage(MESSAGE_NOTIFY_AVATAR_CHANGE, gaiaId);
-// mLoaderThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Channels a change notification event through the loader thread to ensure
-// * proper concurrency.
-// */
-// public void notifyMediaImageChange(MediaImageChangeNotification notification) {
-// ensureHandler();
-// Message msg = mLoaderThreadHandler.obtainMessage(
-// MESSAGE_NOTIFY_MEDIA_IMAGE_CHANGE, notification);
-// mLoaderThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Channels a change notification event through the loader thread to ensure
-// * proper concurrency.
-// */
-// public void notifyRemoteImageChange(RemoteImageChangeNotification notification) {
-// ensureHandler();
-// Message msg = mLoaderThreadHandler.obtainMessage(
-// MESSAGE_NOTIFY_REMOTE_IMAGE_CHANGE, notification);
-// mLoaderThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Receives the above message, loads photos and then sends a message
-// * to the main thread to process them.
-// */
-// @Override
-// public boolean handleMessage(Message msg) {
-// try {
-// switch (msg.what) {
-//// case MESSAGE_PRELOAD_AVATARS:
-//// @SuppressWarnings("unchecked")
-//// List<AvatarRequest> requests = (List<AvatarRequest>) msg.obj;
-//// mPreloadRequests.clear();
-//// mPreloadRequests.addAll(requests);
-//// mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
-//// preloadAvatarsInBackground();
-//// break;
-// case MESSAGE_CONTINUE_PRELOAD:
-//// preloadAvatarsInBackground();
-// break;
-// case MESSAGE_LOAD_IMAGES:
-// loadImagesInBackground();
-// break;
-// case MESSAGE_NOTIFY_AVATAR_CHANGE:
-// sendMessageAvatarChange((String) msg.obj);
-// break;
-// case MESSAGE_NOTIFY_MEDIA_IMAGE_CHANGE:
-// sendMessageMediaImageChange((MediaImageChangeNotification) msg.obj);
-// break;
-// case MESSAGE_NOTIFY_REMOTE_IMAGE_CHANGE:
-// sendMessageRemoteImageChange((RemoteImageChangeNotification) msg.obj);
-// break;
-// }
-// return true;
-// } catch (Throwable t) {
-// Thread.getDefaultUncaughtExceptionHandler()
-// .uncaughtException(Thread.currentThread(), t);
-// return false;
-// }
-// }
-//
-//// /**
-//// * The first time it is called, figures out which photos need to be preloaded.
-//// * Each subsequent call preloads the next batch of photos and requests
-//// * another cycle of preloading after a delay. The whole process ends when
-//// * we either run out of photos to preload or fill up cache.
-//// */
-//// private void preloadAvatarsInBackground() {
-//// if (mPreloadStatus == PRELOAD_STATUS_DONE) {
-//// return;
-//// }
-////
-//// if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
-//// if (mPreloadRequests.isEmpty()) {
-//// mPreloadStatus = PRELOAD_STATUS_DONE;
-//// } else {
-//// mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
-//// }
-//// continuePreloading();
-//// return;
-//// }
-////
-//// if (mImageHolderCache.size() > mImageHolderCacheRedZoneBytes) {
-//// mPreloadStatus = PRELOAD_STATUS_DONE;
-//// return;
-//// }
-////
-//// mRequests.clear();
-////
-//// int count = 0;
-//// int preloadSize = mPreloadRequests.size();
-//// while (preloadSize > 0 && mRequests.size() < PRELOAD_BATCH) {
-//// preloadSize--;
-//// AvatarRequest request = mPreloadRequests.get(preloadSize);
-//// mPreloadRequests.remove(preloadSize);
-////
-//// if (mImageHolderCache.get(request) == null) {
-//// mRequests.add(request);
-//// count++;
-//// }
-//// }
-////
-//// loadImagesFromDatabase(true);
-////
-//// if (preloadSize == 0) {
-//// mPreloadStatus = PRELOAD_STATUS_DONE;
-//// }
-////
-//// if (EsLog.isLoggable(TAG, Log.INFO)) {
-//// Log.v(TAG, "Preloaded " + count + " avatars. "
-//// + "Cache size (bytes): " + mImageHolderCache.size());
-//// }
-////
-//// // Ask to preload the next batch.
-//// continuePreloading();
-//// }
-//
-// /**
-// * Forwards the change notification event to the main thread.
-// */
-// private void sendMessageAvatarChange(String gaiaId) {
-// Message msg = mMainThreadHandler.obtainMessage(MESSAGE_AVATAR_CHANGED, gaiaId);
-// mMainThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Forwards the change notification event to the main thread.
-// */
-// private void sendMessageMediaImageChange(MediaImageChangeNotification notification) {
-// deliverImage(notification.request, notification.imageBytes, true, false);
-// Message msg = mMainThreadHandler.obtainMessage(
-// MESSAGE_MEDIA_IMAGE_CHANGED, notification);
-// mMainThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Forwards the change notification event to the main thread.
-// */
-// private void sendMessageRemoteImageChange(RemoteImageChangeNotification notification) {
-// deliverImage(notification.request, notification.imageBytes, true, false);
-// Message msg = mMainThreadHandler.obtainMessage(
-// MESSAGE_REMOTE_IMAGE_CHANGED, notification);
-// mMainThreadHandler.sendMessage(msg);
-// }
-//
-// /**
-// * Loads photos from the database, puts them in cache and then notifies the UI thread
-// * that they have been loaded.
-// */
-// private void loadImagesInBackground() {
-// obtainRequestsToLoad(mRequests);
-// loadImagesFromDatabase(false);
-// continuePreloading();
-// }
-//
-//// /**
-//// * Loads photos from the database, puts them in cache and then notifies the UI thread
-//// * that they have been loaded.
-//// */
-//// private void loadImagesFromDatabase(boolean preloading) {
-//// int count = mRequests.size();
-//// if (count == 0) {
-//// return;
-//// }
-////
-//// // Remove loaded photos from the preload queue: we don't want
-//// // the preloading process to load them again.
-//// if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
-//// mPreloadRequests.removeAll(mRequests);
-//// if (mPreloadRequests.isEmpty()) {
-//// mPreloadStatus = PRELOAD_STATUS_DONE;
-//// }
-//// }
-////
-//// ArrayList<AvatarRequest> avatarRequests = null;
-//// ArrayList<MediaImageRequest> mediaRequests = null;
-//// ArrayList<EventThemeImageRequest> eventThemeRequests = null;
-//// ArrayList<ImageRequest> remoteRequests = null;
-////
-//// for (ImageRequest request : mRequests) {
-//// if (request instanceof AvatarRequest) {
-//// if (avatarRequests == null) {
-//// avatarRequests = new ArrayList<AvatarRequest>();
-//// }
-//// avatarRequests.add((AvatarRequest) request);
-//// } else if (request instanceof MediaImageRequest) {
-//// if (mediaRequests == null) {
-//// mediaRequests = new ArrayList<MediaImageRequest>();
-//// }
-//// mediaRequests.add((MediaImageRequest) request);
-//// } else if (request instanceof EventThemeImageRequest) {
-//// if (eventThemeRequests == null) {
-//// eventThemeRequests = new ArrayList<EventThemeImageRequest>();
-//// }
-//// eventThemeRequests.add((EventThemeImageRequest) request);
-//// } else {
-//// if (remoteRequests == null) {
-//// remoteRequests = new ArrayList<ImageRequest>();
-//// }
-//// remoteRequests.add(request);
-//// }
-//// }
-////
-//// if (mediaRequests != null) {
-//// Map<MediaImageRequest, byte[]> avatars = EsPostsData.loadMedia(
-//// mContext, mediaRequests);
-////
-//// for (Entry<MediaImageRequest, byte[]> entry : avatars.entrySet()) {
-//// MediaImageRequest request = entry.getKey();
-//// deliverImage(request, entry.getValue(), true, preloading);
-//// mRequests.remove(request);
-//// }
-//// }
-////
-//// if (avatarRequests != null) {
-//// Map<AvatarRequest, byte[]> avatars = EsAvatarData.loadAvatars(
-//// mContext, avatarRequests);
-////
-//// for (Entry<AvatarRequest, byte[]> entry : avatars.entrySet()) {
-//// AvatarRequest request = entry.getKey();
-//// deliverImage(request, entry.getValue(), true, preloading);
-//// mRequests.remove(request);
-//// }
-//// }
-////
-//// if (eventThemeRequests != null) {
-//// for (EventThemeImageRequest request : eventThemeRequests) {
-//// byte[] themeBytes = EsEventData.loadEventTheme(mContext, request);
-//// if (themeBytes != null) {
-//// deliverImage(request, themeBytes, true, false);
-//// mRequests.remove(request);
-//// }
-//// }
-//// }
-////
-//// // NOTE: Do not use the same pattern as other images for the following image.
-//// // Since all of these photos are "local" [either because they're physically
-//// // stored on the device or because they're available through the Picasa
-//// // content provider], there is no need to store them in the database. Just
-//// // throw all of the requests into a loader thread and be done with it.
-//// if (remoteRequests != null) {
-//// final int requestCount = remoteRequests.size();
-//// for (int i = 0; i < requestCount; i++) {
-//// final ImageRequest request = remoteRequests.get(i);
-////
-//// // Only if we still need to load
-//// if (mPendingRequests.containsValue(request)) {
-//// RemoteImageLoader.downloadImage(mContext, request);
-//// }
-//// }
-//// }
-////
-//// // Remaining photos were not found in the database - mark the cache accordingly.
-//// for (ImageRequest request : mRequests) {
-//// deliverImage(request, null, false, preloading);
-//// }
-////
-//// mMainThreadHandler.sendEmptyMessage(MESSAGE_IMAGES_LOADED);
-//// }
-// }
-}
diff --git a/src/com/android/mail/photo/util/ImageProxyUtil.java b/src/com/android/mail/photo/util/ImageProxyUtil.java
deleted file mode 100644
index 11370ad..0000000
--- a/src/com/android/mail/photo/util/ImageProxyUtil.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.util;
-
-import android.net.Uri;
-
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Useful Image Proxy url manipulation routines.
- */
-public class ImageProxyUtil {
- private static final Pattern PROXY_HOSTED_IMAGE_URL_RE =
- Pattern.compile("^(((http(s)?):)?\\/\\/"
- + "images(\\d)?-.+-opensocial\\.googleusercontent\\.com\\/gadgets\\/proxy\\?)");
-
- /** Default container, if we don't already have one */
- static final String DEFAULT_CONTAINER = "esmobile";
-
- static final String PROXY_DOMAIN_PREFIX = "images";
- static final String PROXY_DOMAIN_SUFFIX = "-opensocial.googleusercontent.com";
- static final String PROXY_PATH = "/gadgets/proxy";
- static final String PARAM_URL = "url";
- static final String PARAM_CONTAINER = "container";
- static final String PARAM_GADGET = "gadget";
- static final String PARAM_REWRITE_MIME = "rewriteMime";
- static final String PARAM_REFRESH = "refresh";
- static final String PARAM_HEIGHT = "resize_h";
- static final String PARAM_WIDTH = "resize_w";
- static final String PARAM_QUALITY = "resize_q";
- static final String PARAM_NO_EXPAND = "no_expand";
- static final String PARAM_FALLBACK_URL = "fallback_url";
- static final int PROXY_COUNT = 3;
- static int sProxyIndex;
-
- public static final int ORIGINAL_SIZE = -1;
-
- /**
- * Add size options to the given url.
- *
- * @param size the image size
- * @param url the url to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- public static String setImageUrlSize(int size, String url) {
- if (url == null) {
- return url;
- }
-
- final String proxyUrl;
- if (!isProxyHostedUrl(url)) {
- proxyUrl = createProxyUrl();
- } else {
- proxyUrl = url;
- url = null;
- }
- final Uri proxyUri = Uri.parse(proxyUrl);
- return setImageUrlSizeOptions(size, size, proxyUri, url).toString();
- }
-
-
- /**
- * Add size options to the given url.
- *
- * @param width the image width
- * @param height the image height
- * @param url the url to apply the options to
- * @return a {@code Uri} containting the new image url with options.
- */
- public static String setImageUrlSize(int width, int height, String url) {
- if (url == null) {
- return url;
- }
-
- final String proxyUrl;
- if (!isProxyHostedUrl(url)) {
- proxyUrl = createProxyUrl();
- } else {
- proxyUrl = url;
- url = null;
- }
- final Uri proxyUri = Uri.parse(proxyUrl);
- return setImageUrlSizeOptions(width, height, proxyUri, url).toString();
- }
-
- /**
- * Returns a default proxy URL.
- */
- private static String createProxyUrl() {
- StringBuffer proxy = new StringBuffer();
- proxy.append("http://")
- .append(PROXY_DOMAIN_PREFIX)
- .append(getNextProxyIndex())
- .append("-")
- .append(DEFAULT_CONTAINER)
- .append(PROXY_DOMAIN_SUFFIX)
- .append(PROXY_PATH);
- return proxy.toString();
- }
-
- /**
- * Returns the next proxy index.
- */
- private static synchronized int getNextProxyIndex() {
- int toReturn = ++sProxyIndex;
- sProxyIndex %= PROXY_COUNT;
- return toReturn;
- }
-
- /**
- * Add image url options to the given url.
- *
- * @param width the image width
- * @param height the image height
- * @param proxyUri the uri to apply the options to
- * @return a {@code Uri} containing the image url with the width and height set.
- */
- public static Uri setImageUrlSizeOptions(int width, int height, Uri proxyUri, String imageUrl) {
- Uri.Builder proxyUriBuilder;
- Uri newProxyUri;
-
- proxyUriBuilder = Uri.EMPTY.buildUpon();
- proxyUriBuilder.authority(proxyUri.getAuthority());
- proxyUriBuilder.scheme(proxyUri.getScheme());
- proxyUriBuilder.path(proxyUri.getPath());
- // Set these here to override any settings in the source proxy URI
- if (width != ORIGINAL_SIZE && height != ORIGINAL_SIZE) {
- proxyUriBuilder.appendQueryParameter(PARAM_WIDTH, Integer.toString(width));
- proxyUriBuilder.appendQueryParameter(PARAM_HEIGHT, Integer.toString(height));
- proxyUriBuilder.appendQueryParameter(PARAM_NO_EXPAND, "1");
- }
-
- newProxyUri = proxyUriBuilder.build();
-
- final Set<String> paramNames = getQueryParameterNames(proxyUri);
- for (String key : paramNames) {
- if (newProxyUri.getQueryParameter(key) != null) {
- continue;
- }
-
- proxyUriBuilder = newProxyUri.buildUpon();
- if (PARAM_URL.equals(key)) {
- // Ensure there's only one url parameter
- proxyUriBuilder.appendQueryParameter(PARAM_URL,
- proxyUri.getQueryParameter(PARAM_URL));
-
- } else if ((width == ORIGINAL_SIZE || height == ORIGINAL_SIZE) &&
- (PARAM_WIDTH.equals(key) || PARAM_HEIGHT.equals(key) ||
- PARAM_NO_EXPAND.equals(key))) {
- // Don't allow width / height / no-expand parameters if we ask for a full-size image
- continue;
-
- } else {
- final List<String> values = proxyUri.getQueryParameters(key);
- for (String value : values) {
- proxyUriBuilder.appendQueryParameter(key, value);
- }
- }
- newProxyUri = proxyUriBuilder.build();
- }
-
- // The following parameters are mandatory; make sure the URL has them
- if (imageUrl != null && newProxyUri.getQueryParameter(PARAM_URL) == null) {
- proxyUriBuilder = newProxyUri.buildUpon();
- proxyUriBuilder.appendQueryParameter(PARAM_URL, imageUrl);
- newProxyUri = proxyUriBuilder.build();
- }
- if (newProxyUri.getQueryParameter(PARAM_CONTAINER) == null) {
- proxyUriBuilder = newProxyUri.buildUpon();
- proxyUriBuilder.appendQueryParameter(PARAM_CONTAINER, DEFAULT_CONTAINER);
- newProxyUri = proxyUriBuilder.build();
- }
- if (newProxyUri.getQueryParameter(PARAM_GADGET) == null) {
- proxyUriBuilder = newProxyUri.buildUpon();
- proxyUriBuilder.appendQueryParameter(PARAM_GADGET, "a");
- newProxyUri = proxyUriBuilder.build();
- }
- if (newProxyUri.getQueryParameter(PARAM_REWRITE_MIME) == null) {
- proxyUriBuilder = newProxyUri.buildUpon();
- proxyUriBuilder.appendQueryParameter(PARAM_REWRITE_MIME, "image/*");
- newProxyUri = proxyUriBuilder.build();
- }
-
- return newProxyUri;
- }
-
- /**
- * Backwards-compatible implementation of
- * {@link Uri#getQueryParameterNames()}.
- */
- private static Set<String> getQueryParameterNames(Uri uri) {
- if (uri.isOpaque()) {
- throw new UnsupportedOperationException("This isn't a hierarchical URI.");
- }
-
- String query = uri.getEncodedQuery();
- if (query == null) {
- return Collections.emptySet();
- }
-
- Set<String> names = new LinkedHashSet<String>();
- int start = 0;
- do {
- int next = query.indexOf('&', start);
- int end = (next == -1) ? query.length() : next;
-
- int separator = query.indexOf('=', start);
- if (separator > end || separator == -1) {
- separator = end;
- }
-
- String name = query.substring(start, separator);
- names.add(Uri.decode(name));
-
- // Move start to end of name.
- start = end + 1;
- } while (start < query.length());
-
- return Collections.unmodifiableSet(names);
- }
-
- /**
- * Checks if the host is a valid FIFE host.
- *
- * @param url an image url to check
- *
- * @return {@code true} iff the url has a valid FIFE host
- */
- public static boolean isProxyHostedUrl(String url) {
- if (url == null) {
- return false;
- }
-
- final Matcher matcher = PROXY_HOSTED_IMAGE_URL_RE.matcher(url);
- return matcher.find();
- }
-
- /**
- * Checks if the host is a valid FIFE host.
- *
- * @param uri an image url to check
- *
- * @return {@code true} iff the url has a valid FIFE host
- */
- public static boolean isProxyHostedUri(Uri uri) {
- return isProxyHostedUrl(uri.toString());
- }
-}
diff --git a/src/com/android/mail/photo/util/ImageUtils.java b/src/com/android/mail/photo/util/ImageUtils.java
index 4143d2d..98edacb 100644
--- a/src/com/android/mail/photo/util/ImageUtils.java
+++ b/src/com/android/mail/photo/util/ImageUtils.java
@@ -17,115 +17,32 @@
package com.android.mail.photo.util;
-import android.app.Dialog;
-import android.app.ProgressDialog;
import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
-import android.os.Environment;
-import android.provider.MediaStore.Images;
-import android.provider.MediaStore.Images.ImageColumns;
-import android.provider.MediaStore.Images.Thumbnails;
-import android.provider.MediaStore.MediaColumns;
-import android.text.TextUtils;
-import android.util.Base64;
import android.util.Log;
-import com.android.mail.R;
import com.android.mail.photo.PhotoViewActivity;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
-import java.text.SimpleDateFormat;
-import java.util.Date;
/**
* Image utilities
*/
public class ImageUtils {
- /** Specifies no background colour should be added during image resizing */
- public static int NO_COLOR = 0;
-
- public static final int INSERT_PHOTO_DIALOG_ID = R.id.dialog_insert_photo;
-
- // added from EsService
- public static final int CROP_NONE = 0;
- public static final int CROP_SQUARE = 1;
- public static final int CROP_WIDE = 2;
-
- private static int MICRO_KIND_MAX_DIMENSION = 0;
- private static int MINI_KIND_MAX_DIMENSION = 0;
-
- private static int DEFAULT_JPEG_QUALITY = 90;
-
// Logging
private static final String TAG = "ImageUtils";
- // Paints and modes
- private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
-
- /** The paint used for cropped photos */
- private static final Paint sCropPaint;
- static {
- sCropPaint = new Paint();
- sCropPaint.setAntiAlias(true);
- sCropPaint.setFilterBitmap(true);
- sCropPaint.setDither(true);
- }
-
- private static final Paint sOutStrokePaint = new Paint();
- static {
- sOutStrokePaint.setStrokeWidth(1);
- sOutStrokePaint.setStyle(Paint.Style.STROKE);
- sOutStrokePaint.setColor(0xff999999);
- }
-
- private static final Paint sInStrokePaint = new Paint();
- static {
- sInStrokePaint.setStrokeWidth(1);
- sInStrokePaint.setStyle(Paint.Style.STROKE);
- sInStrokePaint.setColor(0xfff0f0f0);
- }
-
/** Minimum class memory class to use full-res photos */
private final static long MIN_NORMAL_CLASS = 32;
/** Minimum class memory class to use small photos */
private final static long MIN_SMALL_CLASS = 24;
- public static final boolean sUseLowResImages;
- static {
- if (Build.VERSION.SDK_INT >= 11) {
- // On HC and beyond, assume devices are more capable
- sUseLowResImages = false;
- } else {
- if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
- sUseLowResImages = false;
- } else {
- // If we're not in the small class, use low-res [i.e. RGB_565] photos
- sUseLowResImages = true;
- }
- }
- }
public static enum ImageSize {
EXTRA_SMALL,
@@ -153,124 +70,10 @@
}
/**
- * Interface for when a dialog informing about a camera photo insertion
- * should be shown or hidden.
+ * @return true if the MimeType type is image
*/
- public interface InsertCameraPhotoDialogDisplayer {
- public void showInsertCameraPhotoDialog();
- public void hideInsertCameraPhotoDialog();
- }
-
- /**
- * This class cannot be instantiated
- */
- private ImageUtils() {
- }
-
-
- /**
- * Parses an image from a byte array. May return either a Bitmap or
- * a {@link Drawable}.
- *
- * @param data byte array of compressed image data
- * @return The decoded bitmap or {@link Drawable}, or null if the image could not be decoded.
- */
- public static Object decodeMedia(byte[] data) {
- try {
- if (GifDrawable.isGif(data)) {
- return new GifDrawable(data);
- } else {
- return BitmapFactory.decodeByteArray(data, 0, data.length);
- }
- } catch (OutOfMemoryError oome) {
- Log.e(TAG, "ImageUtils#decodeMedia(byte[]) threw an OOME", oome);
- return null;
- }
- }
-
- /**
- * Wrapper around {@link BitmapFactory#decodeByteArray(byte[], int, int)}
- * that returns {@code null} on {@link OutOfMemoryError}.
- *
- * @param data byte array of compressed image data
- * @param offset offset into imageData for where the decoder should begin
- * parsing.
- * @param length the number of bytes, beginning at offset, to parse
- * @return The decoded bitmap, or null if the image could not be decode.
- */
- public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
- try {
- return BitmapFactory.decodeByteArray(data, offset, length);
- } catch (OutOfMemoryError oome) {
- Log.e(TAG, "ImageUtils#decodeByteArray(byte[], int, int) threw an OOME", oome);
- return null;
- }
- }
-
- /**
- * Wrapper around {@link BitmapFactory#decodeByteArray(byte[], int, int,
- * BitmapFactory.Options)} that returns {@code null} on {@link
- * OutOfMemoryError}.
- *
- * @param data byte array of compressed image data
- * @param offset offset into imageData for where the decoder should begin
- * parsing.
- * @param length the number of bytes, beginning at offset, to parse
- * @param opts null-ok; Options that control downsampling and whether the
- * image should be completely decoded, or just is size returned.
- * @return The decoded bitmap, or null if the image could not be decode.
- */
- public static Bitmap decodeByteArray(byte[] data, int offset, int length,
- BitmapFactory.Options opts) {
- try {
- return BitmapFactory.decodeByteArray(data, offset, length, opts);
- } catch (OutOfMemoryError oome) {
- Log.e(TAG, "ImageUtils#decodeByteArray(byte[], int, int, Options) threw an OOME", oome);
- return null;
- }
- }
-
- /**
- * Wrapper around {@link BitmapFactory#decodeResource(Resources, int)}
- * that returns {@code null} on {@link OutOfMemoryError}.
- *
- * @param res The resources object containing the image data
- * @param id The resource id of the image data
- * @return The decoded bitmap, or null if the image could not be decode.
- */
- public static Bitmap decodeResource(Resources res, int id) {
- try {
- return BitmapFactory.decodeResource(res, id);
- } catch (OutOfMemoryError oome) {
- Log.e(TAG, "ImageUtils#decodeResource(Resources, int) threw an OOME", oome);
- return null;
- }
- }
-
- /**
- * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
- * BitmapFactory.Options)} that returns {@code null} on {@link
- * OutOfMemoryError}.
- *
- * @param is The input stream that holds the raw data to be decoded into a
- * bitmap.
- * @param outPadding If not null, return the padding rect for the bitmap if
- * it exists, otherwise set padding to [-1,-1,-1,-1]. If
- * no bitmap is returned (null) then padding is
- * unchanged.
- * @param opts null-ok; Options that control downsampling and whether the
- * image should be completely decoded, or just is size returned.
- * @return The decoded bitmap, or null if the image data could not be
- * decoded, or, if opts is non-null, if opts requested only the
- * size be returned (in opts.outWidth and opts.outHeight)
- */
- public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
- try {
- return BitmapFactory.decodeStream(is, outPadding, opts);
- } catch (OutOfMemoryError oome) {
- Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
- return null;
- }
+ public static boolean isImageMimeType(String mimeType) {
+ return mimeType != null && mimeType.startsWith("image/");
}
/**
@@ -314,832 +117,29 @@
}
/**
- * Creates a bitmap from the given bytes at the specified dimension and with the
- * specified crop. Sub-sample as necessary.
+ * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
+ * BitmapFactory.Options)} that returns {@code null} on {@link
+ * OutOfMemoryError}.
*
- * TODO(toddke) Currently, we only perform the wide crop in this method. The square
- * crop is already handled via the FIFE / Image Proxy URLs. When the photo cache and
- * image cache are merged, we'll need to support square crop as well.
+ * @param is The input stream that holds the raw data to be decoded into a
+ * bitmap.
+ * @param outPadding If not null, return the padding rect for the bitmap if
+ * it exists, otherwise set padding to [-1,-1,-1,-1]. If
+ * no bitmap is returned (null) then padding is
+ * unchanged.
+ * @param opts null-ok; Options that control downsampling and whether the
+ * image should be completely decoded, or just is size returned.
+ * @return The decoded bitmap, or null if the image data could not be
+ * decoded, or, if opts is non-null, if opts requested only the
+ * size be returned (in opts.outWidth and opts.outHeight)
*/
- public static Bitmap createBitmap(byte[] imageBytes, int width, int height, int cropType) {
- if (imageBytes == null || imageBytes.length == 0) {
- return null;
- }
-
- final ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
- final boolean useLowResImages = ImageUtils.sUseLowResImages;
+ public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
try {
- final BitmapFactory.Options opts = new BitmapFactory.Options();
- final Point bounds = getImageBounds(imageBytes);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "PhotoCache#createBitmap; w: " +
- bounds.x + ", h: " + bounds.y + ", max: " + width);
- }
- opts.inSampleSize = Math.max(bounds.x / width, bounds.y / height);
- if (useLowResImages) {
- opts.inPreferredConfig = Config.RGB_565;
- }
-
- final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
- if (decodedBitmap == null) {
- return null;
- }
-
- final Bitmap croppedBitmap;
- if (cropType == CROP_WIDE) { // changed from EsService.CROP_WIDE
- croppedBitmap = cropWideBitmap(decodedBitmap, width, height);
- decodedBitmap.recycle();
-
- if (croppedBitmap == null) {
- return null;
- }
- } else {
- croppedBitmap = decodedBitmap;
- }
-
- if (useLowResImages) {
- final Bitmap lowResBitmap = ImageUtils.getLowResBitmap(croppedBitmap);
- if (lowResBitmap != croppedBitmap) {
- croppedBitmap.recycle();
- }
- return lowResBitmap;
- } else {
- return croppedBitmap;
- }
- } catch (OutOfMemoryError e) {
- // Do nothing - the photo will appear to be missing
- } finally {
- try {
- inputStream.close();
- } catch (IOException ignore) {
- }
- }
- return null;
- }
-
- /**
- * Crops the given bitmap according to the {@link EsService#CROP_WIDE} style. The
- * center of the bitmap is used to create a new bitmap of exactly width x height
- * pixels, maintaining the original aspect ratio. The original bitmap will be
- * cropped and/or enlarged as necessary.
- */
- private static Bitmap cropWideBitmap(Bitmap inputBitmap, int width, int height) {
- final Rect srcRect;
-
- final int srcWidth = inputBitmap.getWidth();
- final int srcHeight = inputBitmap.getHeight();
- final int dstWidth = width;
- final int dstHeight = height;
-
- if (srcWidth == dstWidth && srcHeight == dstHeight) {
- // Photo is exactly the same size as the on-screen image
- srcRect = new Rect(0, 0, srcWidth, srcHeight);
- } else {
- // create a source rectangle of the same aspect ratio as the requested size.
- int cropWidth = srcWidth;
- int cropHeight = srcHeight;
- if (srcWidth * dstHeight > srcHeight * dstWidth) {
- // the input bitmap is a wider aspect ratio. Crop the sides.
- cropWidth = srcHeight * dstWidth / dstHeight;
- } else {
- // The input bitmap is a taller aspect ratio. Crop the top and bottom.
- cropHeight = srcWidth * dstHeight / dstWidth;
- }
-
- final int left = (srcWidth - cropWidth) / 2;
- final int top = (srcHeight - cropHeight) / 2;
- srcRect = new Rect(left, top, left + cropWidth, top + cropHeight);
- }
-
- // Create the new bitmap
- final Bitmap.Config bitmapConfig =
- ImageUtils.sUseLowResImages ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;
- final Bitmap bitmap = Bitmap.createBitmap(width, height, bitmapConfig);
- if (bitmap == null) {
+ return BitmapFactory.decodeStream(is, outPadding, opts);
+ } catch (OutOfMemoryError oome) {
+ Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
}
-
- final Canvas canvas = new Canvas(bitmap);
- final Rect dstRect = new Rect(0, 0, width, height);
-
- synchronized (sCropPaint) {
- canvas.drawBitmap(inputBitmap, srcRect, dstRect, sCropPaint);
- }
-
- return bitmap;
- }
-
- /**
- * Gets the image bounds
- */
- private static Point getImageBounds(byte[] imageBytes) {
- final BitmapFactory.Options opts = new BitmapFactory.Options();
- final ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
-
- try {
- opts.inJustDecodeBounds = true;
- decodeStream(inputStream, null, opts);
- return new Point(opts.outWidth, opts.outHeight);
- } finally {
- try {
- inputStream.close();
- } catch (IOException ignore) {
- }
- }
- }
-
- /**
- * Create a center-cropped bitmap from a uri.
- *
- * @param resolver The ContentResolver
- * @param uri The uri
- * @param width The width of the output bitmap
- * @param height The height of the output bitmap
- *
- * @return the new bitmap
- */
- public static Bitmap createCroppedBitmap(ContentResolver resolver, Uri uri,
- int width, int height) {
- try {
- InputStream inputStream = resolver.openInputStream(uri);
- final BitmapFactory.Options opts = new BitmapFactory.Options();
- opts.inJustDecodeBounds = true;
- decodeStream(inputStream, null, opts);
- inputStream.close();
-
- // use Math.min() here to ensure that each of the image dimensions are
- // >= the target size
- inputStream = resolver.openInputStream(uri);
- opts.inJustDecodeBounds = false;
- opts.inSampleSize = Math.min(opts.outWidth / width, opts.outHeight / height);
- Bitmap srcBitmap = decodeStream(inputStream, null, opts);
- inputStream.close();
- if (srcBitmap == null) {
- return null;
- }
- final int srcWidth = srcBitmap.getWidth();
- final int srcHeight = srcBitmap.getHeight();
-
- if (srcWidth == width && srcHeight == height) {
- return srcBitmap;
- }
-
- Bitmap destBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- if (destBitmap == null) {
- srcBitmap.recycle();
- return null;
- }
-
- final Canvas canvas = new Canvas(destBitmap);
- int croppedWidth = srcWidth;
- int croppedHeight = srcHeight;
- // We want to take the center part of the image with the same aspect
- // ratio as the target, and crop the rest. The same behavior as CENTER_CROP.
- if (srcWidth * height > srcHeight * width) {
- // The input bitmap is a wider aspect ratio. Crop the sides.
- croppedWidth = srcHeight * width / height;
- } else {
- // The input bitmap is a taller aspect ratio. Crop the top and bottom.
- croppedHeight = srcWidth * height / width;
- }
- final int left = (srcWidth - croppedWidth) / 2;
- final int top = (srcHeight - croppedHeight) / 2;
- final Rect src = new Rect(left, top, left + croppedWidth, top + croppedHeight);
- synchronized (sResizePaint) {
- canvas.drawBitmap(srcBitmap, src, new Rect(0, 0, width, height), sResizePaint);
- }
- srcBitmap.recycle();
-
- // correct orientation, as necessary
- return rotateBitmap(resolver, uri, destBitmap);
- } catch (FileNotFoundException exception) {
- return null;
- } catch (IOException exception) {
- return null;
- }
- }
-
- /**
- * Returns the maximum dimension in pixels for a given MediaStore.Images.Thumbnails kind.
- *
- * @param context The context
- * @param kind MICRO_KIND or MINI_KIND
- *
- * @return maxDimension in pixels
- */
- public static int getMaxThumbnailDimension(Context context, int kind) {
- // determine max dimension based on kind
- final int maxDimension;
- switch (kind) {
- case Thumbnails.MICRO_KIND:
- maxDimension = getThumbnailSize(context, Thumbnails.MICRO_KIND);
- break;
-
- case Thumbnails.MINI_KIND:
- maxDimension = getThumbnailSize(context, Thumbnails.MINI_KIND);
- break;
-
- default:
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "illegal kind=" + kind + " specified; using MINI_KIND");
- }
- maxDimension = getThumbnailSize(context, Thumbnails.MINI_KIND);
- break;
- }
- return maxDimension;
- }
-
- /**
- * Convert thumbnail dimensions to pixels
- *
- * @param context The context
- * @param kind The kind
- *
- * @return The size of the thumbnail in pixels
- */
- public static int getThumbnailSize(Context context, int kind) {
- switch (kind) {
- case Thumbnails.MICRO_KIND: {
- if (MICRO_KIND_MAX_DIMENSION == 0) {
- MICRO_KIND_MAX_DIMENSION = context.getResources().getDimensionPixelSize(
- R.dimen.micro_kind_max_dimension);
- }
- return MICRO_KIND_MAX_DIMENSION;
- }
-
- case Thumbnails.MINI_KIND:
- default: {
- if (MINI_KIND_MAX_DIMENSION == 0) {
- MINI_KIND_MAX_DIMENSION = context.getResources().getDimensionPixelSize(
- R.dimen.mini_kind_max_dimension);
- }
- return MINI_KIND_MAX_DIMENSION;
- }
- }
- }
-
- /**
- * Scale a bitmap to a square bitmap
- *
- * @param imageBytes The input bitmap
- * @param size The width and height
- *
- * @return The new bitmap
- */
- public static byte[] resizeToSquareBitmap(byte[] imageBytes, int size) {
- return resizeToSquareBitmap(imageBytes, size, NO_COLOR);
- }
-
- /**
- * Scale a bitmap to a square bitmap
- *
- * @param imageBytes The input bitmap
- * @param size The width and height
- * @param backgroundColor The background color that should be used for translucent avatars.
- *
- * @return The new bitmap
- */
- public static byte[] resizeToSquareBitmap(byte[] imageBytes, int size, int backgroundColor) {
- if (imageBytes == null) {
- return imageBytes;
- }
-
- final BitmapFactory.Options dbo = new BitmapFactory.Options();
- dbo.inJustDecodeBounds = true;
- decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
-
- int nativeWidth = dbo.outWidth;
- int nativeHeight = dbo.outHeight;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "resizeToSquareBitmap: Input: " + nativeWidth + "x" + nativeHeight
- + ", resize to: " + size);
- }
-
- Bitmap bitmap;
- int sampleSize = Math.min(nativeWidth / size, nativeHeight / size);
- if (sampleSize > 1) {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = sampleSize;
- bitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
- } else {
- bitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
- }
-
- if (bitmap == null) {
- return null;
- }
-
- Bitmap scaledBitmap = resizeToSquareBitmap(bitmap, size, backgroundColor);
- bitmap.recycle();
-
- if (scaledBitmap == null) {
- return null;
- }
-
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- scaledBitmap.compress(CompressFormat.JPEG, 80, stream);
- scaledBitmap.recycle();
- scaledBitmap = null;
-
- return stream.toByteArray();
- }
-
- /**
- * Scale a bitmap to a square bitmap
- *
- * @param inputBitmap The input bitmap
- * @param size The width and height
- *
- * @return The new bitmap
- */
- public static Bitmap resizeToSquareBitmap(Bitmap inputBitmap, int size) {
- return resizeToSquareBitmap(inputBitmap, size, NO_COLOR);
- }
-
- /**
- * Scale a bitmap to a square bitmap
- *
- * @param inputBitmap The input bitmap
- * @param size The width and height
- * @param backgroundColor The solid color used to paint the image background. If
- * {@link #NO_COLOR}, no background will be painted.
- *
- * @return The new bitmap
- */
- public static Bitmap resizeToSquareBitmap(Bitmap inputBitmap, int size,
- int backgroundColor) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "resizeToSquareBitmap: Input: " + inputBitmap.getWidth()
- + "x" + inputBitmap.getHeight() + ", output:" + size + "x" + size);
- }
-
- final Bitmap bitmap;
- try {
- // Create the new bitmap
- bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- } catch (OutOfMemoryError e) {
- Log.w(TAG, "resizeToSquareBitmap OutOfMemoryError for image size: " + size, e);
- return null;
- }
-
- if (bitmap == null) {
- return null;
- }
-
- final Canvas canvas = new Canvas(bitmap);
- if (backgroundColor != NO_COLOR) {
- canvas.drawColor(backgroundColor);
- }
-
- if (inputBitmap.getWidth() != size || inputBitmap.getHeight() != size) {
- final Rect src = new Rect(0, 0, inputBitmap.getWidth(), inputBitmap.getHeight());
- final Rect dest = new Rect(0, 0, size, size);
- synchronized(sResizePaint) {
- canvas.drawBitmap(inputBitmap, src, dest, sResizePaint);
- }
- } else {
- canvas.drawBitmap(inputBitmap, 0, 0, null);
- }
-
- return bitmap;
- }
-
- /**
- * Resize and crop a bitmap.
- *
- * @param inputBitmap The input bitmap
- * @param height The height
- * @param width The width
- *
- * @return The new bitmap
- */
- public static Bitmap resizeAndCropBitmap(Bitmap inputBitmap, int width, int height) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "resizeAndCropBitmap: Input: " + inputBitmap.getWidth()
- + "x" + inputBitmap.getHeight() + ", output:"
- + width + "x" + height);
- }
-
- // Create the new bitmap
- final Bitmap bitmap = Bitmap.createBitmap(
- width, height, Bitmap.Config.ARGB_8888);
- if (bitmap == null) {
- return null;
- }
-
- final Canvas canvas = new Canvas(bitmap);
- if (inputBitmap.getWidth() != width || inputBitmap.getHeight() != height) {
- // create a source rectangle of the same aspect ratio as the requested size.
- int croppedWidth = inputBitmap.getWidth();
- int croppedHeight = inputBitmap.getHeight();
- if (inputBitmap.getWidth() * height > inputBitmap.getHeight() * width) {
- // the input bitmap is a wider aspect ratio. Crop the sides.
- croppedWidth = inputBitmap.getHeight() * width / height;
- } else {
- // The input bitmap is a taller aspect ratio. Crop the top and bottom.
- croppedHeight = inputBitmap.getWidth() * height / width;
- }
-
- int left = (inputBitmap.getWidth() - croppedWidth) / 2;
- int top = (inputBitmap.getHeight() - croppedHeight) / 2;
- final Rect src = new Rect(left, top,
- left + croppedWidth, top + croppedHeight);
- final Rect dest = new Rect(0, 0, width, height);
- synchronized(sResizePaint) {
- canvas.drawBitmap(inputBitmap, src, dest, sResizePaint);
- }
- } else {
- canvas.drawBitmap(inputBitmap, 0, 0, null);
- }
-
- return bitmap;
- }
-
- /**
- * Resize a bitmap
- *
- * @param imageBytes The image bytes
- * @param width The width of the resized image
- * @param height The width of the resized image
- *
- * @return The resized bitmap
- */
- public static Bitmap resizeBitmap(byte[] imageBytes, int width, int height) {
- final BitmapFactory.Options dbo = new BitmapFactory.Options();
- dbo.inJustDecodeBounds = true;
- decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
-
- final int nativeWidth = dbo.outWidth;
- final int nativeHeight = dbo.outHeight;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "resizeBitmap: Input: " + nativeWidth + "x" + nativeHeight
- + ", resize to: " + width + "x" + height);
- }
-
- final Bitmap srcBitmap;
- if (nativeWidth > width || nativeHeight > height) {
- final float bitmapWidth = (nativeWidth * width) / nativeHeight;
- final float bitmapHeight = (nativeHeight * height) / nativeWidth;
-
- if (nativeWidth / bitmapWidth > 1 || nativeHeight / bitmapHeight > 1) {
- // Create a scaled bitmap
- final BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = Math.max(nativeWidth / (int)bitmapWidth,
- nativeHeight / (int)bitmapHeight);
- srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
- } else {
- srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
- }
- } else {
- srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
- }
-
- if (srcBitmap == null) {
- return null;
- }
-
- // Crop the bitmap
- final Bitmap croppedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- if (croppedBitmap == null) {
- srcBitmap.recycle();
- return null;
- }
-
- final int srcWidth = srcBitmap.getWidth();
- final int srcHeight = srcBitmap.getHeight();
-
- int croppedWidth = srcWidth;
- int croppedHeight = srcHeight;
- if (nativeWidth * height > width * nativeHeight) {
- // the input bitmap is a wider aspect ratio. Crop the sides.
- croppedWidth = srcBitmap.getHeight() * width / height;
- } else {
- // the input bitmap is a taller aspect ratio. Crop the top and bottom.
- croppedHeight = srcBitmap.getWidth() * height / width;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "resizeBitmap: cropped: " + croppedWidth + "x" + croppedHeight);
- }
-
- final int srcLeft = (srcWidth - croppedWidth) / 2;
- final int srcTop = (srcHeight - croppedHeight) / 2;
- final Rect src = new Rect(srcLeft, srcTop, srcLeft + croppedWidth, srcTop + croppedHeight);
- final Rect dest = new Rect(0, 0, width, height);
-
- final Canvas croppedCanvas = new Canvas(croppedBitmap);
- croppedCanvas.drawColor(0xffe0e0e0);
- synchronized (sResizePaint) {
- croppedCanvas.drawBitmap(srcBitmap, src, dest, sResizePaint);
- }
-
- srcBitmap.recycle();
-
- return croppedBitmap;
- }
-
- /**
- * Resize the bitmap so that its height does not exceed the supplied value.
- *
- * @param imageBytes The image bytes
- * @param height The maximum height of the scaled image
- *
- * @return The resized bitmap as bytes
- */
- public static byte[] resizeBitmapToHeight(byte[] imageBytes, int height) {
- if (imageBytes == null) {
- return imageBytes;
- }
-
- final BitmapFactory.Options dbo = new BitmapFactory.Options();
- dbo.inJustDecodeBounds = true;
- decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
-
- int nativeWidth = dbo.outWidth;
- int nativeHeight = dbo.outHeight;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "scaleBitmap: Input: " + nativeWidth + "x" + nativeHeight
- + ", resize to: " + height);
- }
-
- if (nativeHeight <= height) {
- return imageBytes;
- }
-
- int width = (int) ((float) nativeWidth / nativeHeight * height);
- Bitmap bitmap;
- if (nativeWidth / width > 1 || nativeHeight / height > 1) {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = Math.max(nativeWidth / width, nativeHeight / height);
- bitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
- if (bitmap == null) {
- return null;
- }
- nativeWidth = bitmap.getWidth();
- nativeHeight = bitmap.getHeight();
- } else {
- bitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
- if (bitmap == null) {
- return null;
- }
- }
-
- Bitmap scaledBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- if (scaledBitmap == null) {
- bitmap.recycle();
- return null;
- }
-
- final Canvas canvas = new Canvas(scaledBitmap);
- synchronized (sResizePaint) {
- canvas.drawBitmap(bitmap, new Rect(0, 0, nativeWidth, nativeHeight),
- new Rect(0, 0, width, height), sResizePaint);
- }
- bitmap.recycle();
- bitmap = null;
-
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- scaledBitmap.compress(CompressFormat.PNG, 100, stream);
- scaledBitmap.recycle();
- scaledBitmap = null;
-
- return stream.toByteArray();
- }
-
- /**
- * @param context The context
- * @return A {@link ProgressDialog} informing the user a photo is being
- * inserted
- */
- public static Dialog createInsertCameraPhotoDialog(Context context) {
- final ProgressDialog dialog = new ProgressDialog(context);
- dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
- dialog.setCancelable(false);
- dialog.setMessage(context.getString(R.string.dialog_inserting_camera_photo));
- return dialog;
- }
-
- /**
- * Inserts a newly taken photo into the media store. We cannot directly use
- * {@code Images.Media#insertImage(ContentResolver, String, String, String)}
- * as this method will not properly set the photo's timestamp. Additionally,
- * any EXIF information in the original image is lost and there's a much higher
- * chance for an OOME as insertImage() actually decodes the JPEG just to
- * immediately re-encode it back to a JPEG.
- * <p>
- * NOTE: This code was shamelessly copied and merged from the Camera app
- * [see method addImage() in Storage.java] and Images.Media#insertImage().
- *
- * NOTE: This method should not be called from the UI thread. It performs
- * file IO and generates a thumbnail.
- *
- * @param context The context
- * @param filename The name of the photo
- * @return The media URL of the photo
- * @throws FileNotFoundException If the file is not found
- */
- public static String insertCameraPhoto(Context context, String filename)
- throws FileNotFoundException {
- final File f = new File(Environment.getExternalStorageDirectory(), filename);
-
- final long dateTaken = System.currentTimeMillis();
- final String photoName = createPhotoName(context, dateTaken);
- final ContentResolver resolver = context.getContentResolver();
-
- // Insert into MediaStore
- final ContentValues values = new ContentValues(5);
- final int orientation = ImageUtils.getExifRotation(resolver, f.getAbsolutePath());
-
- values.put(ImageColumns.TITLE, photoName);
- values.put(ImageColumns.DISPLAY_NAME, photoName + ".jpg");
- values.put(ImageColumns.DATE_TAKEN, dateTaken);
- values.put(ImageColumns.MIME_TYPE, "image/jpeg");
- values.put(ImageColumns.ORIENTATION, orientation);
-
- // TODO(kkiyohara): be smarter about figuring out what storage is available, or
- // maybe preventing the photo from being taken if the SD card (external storage)
- // is missing.
- Uri mediaUri;
- try {
- mediaUri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
- } catch (Exception e1) {
- // here when saving to external failed, try internal
- try {
- mediaUri = resolver.insert(Images.Media.INTERNAL_CONTENT_URI, values);
- } catch (Exception e2) {
- try {
- // last chance, try save to HTC-specific PhoneStorage
- mediaUri = resolver.insert(MediaStoreUtils.PHONE_STORAGE_IMAGES_URI, values);
- } catch (Exception e3) {
- Log.e(TAG, "Failed to save image", e3);
- return null;
- }
- }
- }
-
- try {
- // On some platforms this method may throw a NullPointerException
- final OutputStream imageOut = resolver.openOutputStream(mediaUri);
- final FileInputStream imageIn = new FileInputStream(f);
-
- try {
- final int downloadBufferSize = 10240;
- final byte[] array = new byte[downloadBufferSize];
- int bytesRead;
-
- do {
- bytesRead = imageIn.read(array);
- if (bytesRead == -1) {
- break;
- }
- imageOut.write(array, 0, bytesRead);
- } while (true);
- } finally {
- imageOut.close();
- }
-
- // Wait until MINI_KIND thumbnail is generated.
- //
- // If Images.Media.EXTERNAL_CONTENT_URI is not writable, then
- // it is not possible to generate the thumbnail using public APIs.
- if (MediaStoreUtils.isExternalMediaStoreUri(mediaUri)) {
- Bitmap bmp = MediaStoreUtils.getThumbnail(
- context, mediaUri, Images.Thumbnails.MINI_KIND);
- bmp.recycle();
- bmp = null;
- }
- } catch (FileNotFoundException fe) {
- Log.e(TAG, "File not found", fe);
- throw fe;
- } catch (Exception e) {
- Log.e(TAG, "Failed to insert image", e);
- if (mediaUri != null) {
- resolver.delete(mediaUri, null, null);
- mediaUri = null;
- }
- } finally {
- f.delete();
- }
-
- return (mediaUri == null ? null : mediaUri.toString());
- }
-
- /**
- * Returns a a name that is consistent with the Android camera application.
- */
- private static String createPhotoName(Context context, long dateTaken) {
- final Date date = new Date(dateTaken);
- final SimpleDateFormat dateFormat =
- new SimpleDateFormat(context.getString(R.string.image_file_name_format));
-
- return dateFormat.format(date);
- }
-
- /**
- * Gets a URL that can be used to download an image at the given size. The size specifies
- * the maximum width or height of the image. If the given URL is either a FIFE URL or an
- * Image Proxy URL, it will be modified to contain the proper sizing parameters. Otherwise,
- * the URL will be converted to an Image Proxy URL.
- *
- * @return A URL that can be used to retrieve an image of the given size.
- */
- public static String getResizedUrl(int size, String url) {
- if (FIFEUtil.isFifeHostedUrl(url)) {
- return FIFEUtil.setImageUrlSize(size, url, false);
- } else {
- return ImageProxyUtil.setImageUrlSize(size, url);
- }
- }
-
- /**
- * Gets a URL that can be used to download an image at the given size. The size specifies
- * the maximum width or height of the image. If the given URL is either a FIFE URL or an
- * Image Proxy URL, it will be modified to contain the proper sizing parameters. Otherwise,
- * the URL will be converted to an Image Proxy URL.
- *
- * @return A URL that can be used to retrieve an image of the given size.
- */
- public static String getResizedUrl(int width, int height, String url) {
- if (FIFEUtil.isFifeHostedUrl(url)) {
- return FIFEUtil.setImageUrlSize(width, height, url, false, false);
- } else {
- return ImageProxyUtil.setImageUrlSize(width, height, url);
- }
- }
-
- /**
- * See {@link #getCroppedAndResizedUrl(int, String)} for more information. This method
- * differs from getCroppedAndResizedUrl because it attempts to get a center cropped
- * version of the requested image. This is only possible for FIFE hosted URLs; Image
- * Proxy URLs will work as they do in getCroppedAndResizedUrl.
- *
- * @return A URL that can be used to retrieve an image of the given size.
- */
- public static String getCenterCroppedAndResizedUrl(int width, int height, String url) {
- if (url == null) {
- return null;
- }
-
- if (FIFEUtil.isFifeHostedUrl(url)) {
- final StringBuilder options = new StringBuilder();
- options.append("w").append(width);
- options.append("-h").append(height);
- options.append("-d");
- options.append("-n");
- return FIFEUtil.setImageUrlOptions(options.toString(), url).toString();
- } else {
- return ImageProxyUtil.setImageUrlSize(width, height, url);
- }
- }
-
- /**
- * See {@link #getResizedUrl(int, String)} for more information. This method differs
- * from getResizedUrl because it attempts to get a cropped version of the requested
- * image, meaning that for a given size, the returned image will be of dimension size
- * in both x and y. This is only possible for FIFE hosted URLs; Image Proxy URLs will
- * work as they do in getResizedUrl.
- *
- * @param size The size
- * @param url The URL
- * @return A URL that can be used to retrieve an image of the given size,
- * cropped if possible.
- */
- public static String getCroppedAndResizedUrl(int size, String url) {
- if (FIFEUtil.isFifeHostedUrl(url)) {
- return FIFEUtil.setImageUrlSize(size, url, true);
- } else {
- // The image proxy has no facility to crop images
- return ImageProxyUtil.setImageUrlSize(size, url);
- }
- }
-
- /**
- * For some images, namely PNG images, the decode ignores the preferred config option and
- * always decodes them as 32bpp. On devices that will see the most benefit, we re-encode
- * the image as 16bpp. Otherwise, prefer to have greater fidelity in a PNG. The specified
- * bitmap will be recycled automatically as necessary.
- */
- public static Bitmap getLowResBitmap(Bitmap bitmap) {
- if (bitmap == null) {
- return null;
- }
-
- if (bitmap.getConfig() == Config.ARGB_8888) {
- final int width = bitmap.getWidth();
- final int height = bitmap.getHeight();
- final Bitmap lowResBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
- final Canvas canvas = new Canvas(lowResBitmap);
- final Rect src = new Rect(0, 0, width, height);
- final Rect dest = new Rect(0, 0, width, height);
-
- synchronized(sResizePaint) {
- canvas.drawBitmap(bitmap, src, dest, sResizePaint);
- }
- bitmap.recycle();
- return lowResBitmap;
- }
- return bitmap;
}
/**
@@ -1170,208 +170,4 @@
}
}
}
-
- /**
- * Get the file path of a media item
- *
- * @return the filepath for a given MediaStore uri, or null if there was a
- * problem
- */
- private static String getFilePath(ContentResolver resolver, Uri uri) {
- // Ask MediaStore for the actual file path
- final Cursor cursor = resolver.query(uri,
- new String[] {MediaColumns._ID, MediaColumns.DATA}, null, null, null);
- if (cursor == null) {
- Log.w(TAG, "getFilePath: query returned null cursor for uri=" + uri);
- return null;
- }
-
- String path = null;
- try {
- if (!cursor.moveToFirst()) {
- Log.w(TAG, "getFilePath: query returned empty cursor for uri=" + uri);
- return null;
- }
-
- // Get the file path
- path = cursor.getString(cursor.getColumnIndexOrThrow(MediaColumns.DATA));
- if (TextUtils.isEmpty(path)) {
- Log.w(TAG, "getFilePath: MediaColumns.DATA was empty for uri=" + uri);
- return null;
- }
- } finally {
- cursor.close();
- }
-
- return path;
- }
-
- /**
- * Encode the given image as a Base64 string (recycle the bitmap)
- *
- * @param imageBytes The image bytes
- *
- * @return A base64 encoded string
- */
- public static String encodeImageBytes(byte[] imageBytes) {
- String base64 = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
- return "data:image/jpeg;base64," + base64;
- }
-
- /**
- * Decode an image from a Base64 string
- *
- * @param string A base64 encoded string
- *
- * @return The image bytes
- */
- public static byte[] decodeImageBytes(String string) {
- int start = string.indexOf("base64,");
- if (start == -1) {
- return null;
- }
-
- return Base64.decode(string.substring(start+7), Base64.DEFAULT);
- }
-
- /**
- * Compress the bitmap to JPEG and return the compressed image bytes. The given bitmap will
- * be recycled.
- *
- * @param bitmap The bitmap
- * @param quality the quality level for JPEG coding (90 is default).
- *
- * @return The compressed image bytes
- */
- public static byte[] compressBitmap(Bitmap bitmap, int quality) {
- final ByteArrayOutputStream stream = new ByteArrayOutputStream();
- try {
- bitmap.compress(CompressFormat.JPEG, quality, stream); // Copy #1
- stream.flush();
- } catch (IOException ignore) {
- } finally {
- try {
- stream.close();
- } catch (IOException ignore) {
- }
- }
- bitmap.recycle();
- bitmap = null;
-
- final byte[] imageBytes = stream.toByteArray(); // Copy #2
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "compressBitmap: Image size: " + imageBytes.length);
- }
- return imageBytes;
- }
-
- /**
- * Compress the bitmap to JPEG and return the compressed image bytes. The given bitmap will
- * be recycled. A default quality level of 90 is used.
- *
- * @param bitmap The bitmap
- *
- * @return The compressed image bytes
- */
- public static byte[] compressBitmap(Bitmap bitmap) {
- return compressBitmap(bitmap, DEFAULT_JPEG_QUALITY);
- }
-
- /**
- * Retrieve the EXIF rotation of an image
- *
- * @param cr the content resolver, only used when the path given is an
- * actual content uri.
- * @param path an absolute file path to the photo for which we want to get
- * the rotation angle. Can also be a content uri, in which case
- * the content resolver is used.
- *
- * @return the number of degrees an image needs to be rotated to face the
- * "correct" way. Does this by reading the actual file's EXIF
- * metadata.
- */
- private static int getExifRotation(ContentResolver cr, String path) {
- // create the Exif interface
- ExifInterface exif = null;
- try {
- exif = new ExifInterface(path);
- } catch (IOException e) {
- Log.w(TAG, "failed to create ExifInterface for " + path);
- }
-
- if (exif == null) {
- return 0;
- }
-
- // get and translate the orientation
- int orientation = exif.getAttributeInt(
- ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
-
- int degrees = 0;
- switch (orientation) {
- case ExifInterface.ORIENTATION_NORMAL:
- degrees = 0;
- break;
-
- case ExifInterface.ORIENTATION_ROTATE_90:
- degrees = 90;
- break;
-
- case ExifInterface.ORIENTATION_ROTATE_180:
- degrees = 180;
- break;
-
- case ExifInterface.ORIENTATION_ROTATE_270:
- degrees = 270;
- break;
- }
-
- return degrees;
- }
-
- /**
- * Rotate a bitmap based on the MediaStore uri's EXIF information.
- *
- * @param cr standard content resolver
- * @param uri MediaStore uri
- * @param bmp bitmap to rotated
- * @return bitmap with proper orientation
- */
- public static Bitmap rotateBitmap(ContentResolver cr, Uri uri, Bitmap bmp) {
- if (bmp != null) {
- final String path = getFilePath(cr, uri);
- final int degrees = getExifRotation(cr, path);
- if (degrees != 0) {
- bmp = rotateBitmap(bmp, degrees);
- }
- }
- return bmp;
- }
-
- /**
- * Bitmap rotation method
- *
- * @param bitmap The input bitmap
- * @param degrees The rotation angle
- */
- private static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
- if (degrees != 0 && bitmap != null) {
- final Matrix m = new Matrix();
- final int w = bitmap.getWidth();
- final int h = bitmap.getHeight();
- m.setRotate(degrees, (float) w / 2, (float) h / 2);
-
- try {
- final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true);
- if (bitmap != rotatedBitmap) {
- bitmap.recycle();
- bitmap = rotatedBitmap;
- }
- } catch (OutOfMemoryError ex) {
- // We have no memory to rotate. Return the original bitmap.
- }
- }
-
- return bitmap;
- }
}
diff --git a/src/com/android/mail/photo/util/MediaStoreUtils.java b/src/com/android/mail/photo/util/MediaStoreUtils.java
deleted file mode 100644
index e1650e8..0000000
--- a/src/com/android/mail/photo/util/MediaStoreUtils.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.photo.util;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Utilities for MediaStore.
- */
-public class MediaStoreUtils {
- public static final String TAG = "MediaStoreUtils";
-
- // Special HTC-only MediaStore storage volume
- public static final Uri PHONE_STORAGE_IMAGES_URI =
- MediaStore.Images.Media.getContentUri("phoneStorage");
-
- public static final Uri PHONE_STORAGE_VIDEO_URI =
- MediaStore.Video.Media.getContentUri("phoneStorage");
-
- /**
- * Define constants for Video info query.
- */
- @SuppressWarnings("unused")
- private static interface VideoQuery {
- /** Projection of the VideoQuery cursors */
- public static final String[] PROJECTION = {
- BaseColumns._ID,
- MediaStore.Video.VideoColumns.DURATION,
- MediaStore.Video.VideoColumns.RESOLUTION,
- };
-
- public static final int INDEX_ID = 0;
- public static final int INDEX_DURATION_MSEC = 1;
- public static final int INDEX_RESOLUTION = 2;
- }
-
- /** regex used to parse video resolution "XxY" -- never trust MediaStore! */
- private static final Pattern PAT_RESOLUTION = Pattern.compile("(\\d+)[xX](\\d+)");
-
- /**
- * Prevent instantiation
- */
- private MediaStoreUtils() {
- }
-
- /**
- * Check if a URI is from the MediaStore
- *
- * @param uri The URI
- */
- public static boolean isMediaStoreUri(Uri uri) {
- return uri != null
- && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
- && MediaStore.AUTHORITY.equals(uri.getAuthority());
- }
-
- /**
- * Checks if a {@link Uri} is an external {@link MediaStore} URI.
- * <p>
- * The {@code getThumbnail} methods of {@link MediaStore} are hard-coded to
- * only support external media URIs. There is an API for loading internal
- * thumbnails, but it is not public and the code cannot be copied easily.
- *
- * @param uri a content URI.
- * @return {@code true} if the {@link Uri} belongs to {@link MediaStore} and
- * is external, {@code false} otherwise.
- * @throws NullPointerException if the argument is {@code null}.
- * @see android.provider.MediaStore.Images.Media#EXTERNAL_CONTENT_URI
- * @see android.provider.MediaStore.Video.Media#EXTERNAL_CONTENT_URI
- */
- public static boolean isExternalMediaStoreUri(Uri uri) {
- if (isMediaStoreUri(uri)) {
- String path = uri.getPath();
- String externalImagePrefix = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPath();
- String externalVideoPrefix = MediaStore.Video.Media.EXTERNAL_CONTENT_URI.getPath();
- return path.startsWith(externalImagePrefix) || path.startsWith(externalVideoPrefix);
- } else {
- return false;
- }
- }
-
- /**
- * @return true if the MimeType type is image
- */
- public static boolean isImageMimeType(String mimeType) {
- return mimeType != null && mimeType.startsWith("image/");
- }
-
- /**
- * @return true if the MimeType type is video
- */
- public static boolean isVideoMimeType(String mimeType) {
- return mimeType != null && mimeType.startsWith("video/");
- }
-
- /**
- * Gets the MediaStore thumbnail bitmap for an image or video.
- *
- * @param context this can be an Application Context
- * @param uri image or video Uri
- * @param kind MediaStore.{Images|Video}.Thumbnails.MINI_KIND or MICRO_KIND
- * @return thumbnail bitmap or null
- */
- public static Bitmap getThumbnail(Context context, Uri uri, int kind) {
- // determine actual pixel dimensions
- final int microSize = ImageUtils.getMaxThumbnailDimension(context, kind);
- return getThumbnailHelper(context, uri, microSize, microSize, kind);
- }
-
- /**
- * Gets the MediaStore thumbnail bitmap for an image or video.
- *
- * @param context this can be an Application Context
- * @param uri image or video Uri
- * @param width desired output width
- * @param height desired output height
- * @return thumbnail bitmap or null
- */
- public static Bitmap getThumbnail(Context context, Uri uri, int width, int height) {
- // determine if we want mini or micro thumbnails
- final int microSize = ImageUtils.getMaxThumbnailDimension(context,
- MediaStore.Images.Thumbnails.MICRO_KIND);
- int kind = (width > microSize || height > microSize)
- ? MediaStore.Images.Thumbnails.MINI_KIND
- : MediaStore.Images.Thumbnails.MICRO_KIND;
-
- return getThumbnailHelper(context, uri, width, height, kind);
- }
-
- /**
- * Deletes the MediaStore entry and, as necessary on some pre-ICS devices, corresponding
- * native file
- *
- * @param resolver context reolver
- * @param localContentUri image or video Uri
- * @return true if delete succeeds, false otherwise
- */
- public static boolean deleteLocalFileAndMediaStore(ContentResolver resolver,
- Uri localContentUri) {
- final String filePath = MediaStoreUtils.getFilePath(resolver, localContentUri);
-
- boolean status = resolver.delete(localContentUri, null, null) == 1;
-
- if (status && filePath != null) {
- final File file = new File(filePath);
- if (file.exists()) {
- status = file.delete();
- }
- }
-
- return status;
- }
-
- /**
- * Safe method to retrieve mimetype of a content Uri.
- *
- * On some phones, getType() can throw an exception for no good reason.
- *
- * @param resolver is a standard ContentResolver
- * @param uri is a the target content Uri
- * @return valid mime-type; null if type was unknown or an exception was thrown
- */
- public static String safeGetMimeType(ContentResolver resolver, Uri uri) {
- String mimeType = null;
- try {
- mimeType = resolver.getType(uri);
- } catch (Exception e) {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "safeGetMimeType failed for uri=" + uri, e);
- }
- }
- return mimeType;
- }
-
-// /**
-// * Converts a MediaStore video Uri to VideoData proto byte array.
-// *
-// * @param context can be an ApplicationContext
-// * @param uri is a MediaStore Video Uri
-// * @return byte[] proto byte array, or null if Uri is not a MediaStore video
-// */
-// public static byte[] toVideoDataBytes(Context context, Uri uri) {
-// final VideoData videoData = toVideoData(context, uri);
-// return videoData == null ? null : videoData.toByteArray();
-// }
-//
-// /**
-// * Converts a MediaStore video Uri to an array of VideoData proto.
-// *
-// * @param context can be an ApplicationContext
-// * @param uri is a MediaStore Video Uri
-// * @return VideoData proto byte array, or null if Uri is not a MediaStore video
-// */
-// public static VideoData toVideoData(Context context, Uri uri) {
-// // see if this is a video
-// final ContentResolver cr = context.getContentResolver();
-// if (!MediaStoreUtils.isVideoMimeType(safeGetMimeType(cr, uri))) {
-// return null;
-// }
-//
-// // format VideoStream info
-// final VideoStream.Builder vs = VideoStream.newBuilder();
-// vs.setStreamUrl(uri.toString());
-//
-// // 0 == unknown format, see ContentHeader.VideoFormat.INVALID_VIDEO_FORMAT
-// vs.setFormatId(0);
-//
-// // query for resolution -- string formatted as "XxY"
-// int width = 0;
-// int height = 0;
-// long durationMsec = 0L;
-// final Cursor cursor = cr.query(uri, VideoQuery.PROJECTION, null, null, null);
-// if (cursor != null) {
-// try {
-// if (cursor.moveToFirst()) {
-// durationMsec = cursor.getLong(VideoQuery.INDEX_DURATION_MSEC);
-//
-// final String resolution = cursor.getString(VideoQuery.INDEX_RESOLUTION);
-// if (resolution != null) {
-// final Matcher m = PAT_RESOLUTION.matcher(resolution);
-// if (m.find()) {
-// width = Integer.parseInt(m.group(1));
-// height = Integer.parseInt(m.group(2));
-// }
-// }
-// }
-// } finally {
-// cursor.close();
-// }
-// }
-// vs.setVideoWidth(width);
-// vs.setVideoHeight(height);
-//
-// // manufacture VideoData bytes
-// final VideoData vd = VideoData.newBuilder()
-// .setStatus(VideoStatus.FINAL)
-// .setDuration(durationMsec)
-// .addStream(vs)
-// .build();
-// return vd;
-// }
-
- /**
- * @return the file path for a given MediaStore uri, or null if there was a problem
- */
- private static String getFilePath(ContentResolver cr, Uri uri) {
- // ask MediaStore for the actual file path
- Cursor cursor = cr.query(uri, new String [] {MediaStore.MediaColumns.DATA},
- null, null, null);
- if (cursor == null) {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "getFilePath: query returned null cursor for uri=" + uri);
- }
- return null;
- }
-
- String path = null;
- try {
- if (!cursor.moveToFirst()) {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "getFilePath: query returned empty cursor for uri=" + uri);
- }
- return null;
- }
- // read the file path
- path = cursor.getString(0);
- if (TextUtils.isEmpty(path)) {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "getFilePath: MediaColumns.DATA was empty for uri=" + uri);
- }
- return null;
- }
-
- } finally {
- cursor.close();
- }
- return path;
- }
-
- /**
- * Gets the MediaStore thumbnail bitmap for an image or video.
- *
- * @param context this can be an Application Context
- * @param uri image or video URI
- * @param width desired output width
- * @param height desired output height
- * @param kind MediaStore.{Images|Video}.Thumbnails.MINI_KIND or MICRO_KIND
- * @return the thumb nail image, or {@code null}
- */
- private static Bitmap getThumbnailHelper(
- Context context, Uri uri, int width, int height, int kind) {
- // guard against bogus Uri's
- if (uri == null) {
- return null;
- }
-
- // Thumb nails are only available for external media URIs
- if (!isExternalMediaStoreUri(uri)) {
- return null;
- }
-
- final ContentResolver cr = context.getContentResolver();
- final long id = ContentUris.parseId(uri);
-
- // query the appropriate MediaStore thumb nail provider
- final String mimeType = safeGetMimeType(cr, uri);
- Bitmap bmp;
- if (isImageMimeType(mimeType)) {
- bmp = MediaStore.Images.Thumbnails.getThumbnail(cr, id, kind, null);
-
- } else if (isVideoMimeType(mimeType)) {
- bmp = MediaStore.Video.Thumbnails.getThumbnail(cr, id, kind, null);
-
- } else {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "getThumbnail: unrecognized mimeType=" + mimeType + ", uri=" + uri);
- }
- return null;
- }
-
- // if we got the thumb nail, we still have to rotate and crop as necessary
- if (bmp != null) {
- bmp = ImageUtils.rotateBitmap(cr, uri, bmp);
-
- if (bmp.getWidth() != width || bmp.getHeight() != height) {
- final Bitmap resizedBitmap = ImageUtils.resizeAndCropBitmap(
- bmp, width, height);
- bmp.recycle();
- bmp = resizedBitmap;
- }
- }
- return bmp;
- }
-}
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index ef3dc6c..905d093 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -52,6 +52,10 @@
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.EditSettingsExtras;
+import com.google.android.common.html.parser.HtmlDocument;
+import com.google.android.common.html.parser.HtmlParser;
+import com.google.android.common.html.parser.HtmlTree;
+import com.google.android.common.html.parser.HtmlTreeBuilder;
import com.google.common.collect.Maps;
import org.json.JSONObject;
@@ -508,6 +512,29 @@
}
/**
+ * Returns displayable text from the provided HTML string.
+ *
+ * @param htmlText HTML string
+ * @return Plain text string representation of the specified Html string
+ */
+ public static String convertHtmlToPlainText(String htmlText) {
+ return getHtmlTree(htmlText).getPlainText();
+ }
+
+ /**
+ * Returns a {@link HtmlTree} representation of the specified HTML string.
+ */
+ public static HtmlTree getHtmlTree(String htmlText) {
+ final HtmlParser parser = new HtmlParser();
+ final HtmlDocument doc = parser.parse(htmlText);
+ final HtmlTreeBuilder builder = new HtmlTreeBuilder();
+ doc.accept(builder);
+
+ return builder.getTree();
+ }
+
+
+ /**
* Perform a simulated measure pass on the given child view, assuming the
* child has a ViewGroup parent and that it should be laid out within that
* parent with a matching width but variable height. Code largely lifted