Add new intent/method for cropping and setting wallpapers

Also, create a system fallback WallpaperCropper

Bug: 4225598

Change-Id: I6bc6d5a3bb3df1dc00f3db701978aa172020c568
This commit is contained in:
Michael Jurka
2013-09-09 15:58:54 +02:00
parent 49580cf1f7
commit e8d1bf7a43
46 changed files with 11750 additions and 3 deletions

View File

@@ -4364,6 +4364,7 @@ package android.app {
method public void clear() throws java.io.IOException;
method public void clearWallpaperOffsets(android.os.IBinder);
method public void forgetLoadedWallpaper();
method public android.content.Intent getCropAndSetWallpaperIntent(android.net.Uri);
method public int getDesiredMinimumHeight();
method public int getDesiredMinimumWidth();
method public android.graphics.drawable.Drawable getDrawable();
@@ -4381,6 +4382,7 @@ package android.app {
method public void setWallpaperOffsets(android.os.IBinder, float, float);
method public void suggestDesiredDimensions(int, int);
field public static final java.lang.String ACTION_CHANGE_LIVE_WALLPAPER = "android.service.wallpaper.CHANGE_LIVE_WALLPAPER";
field public static final java.lang.String ACTION_CROP_AND_SET_WALLPAPER = "android.service.wallpaper.CROP_AND_SET_WALLPAPER";
field public static final java.lang.String ACTION_LIVE_WALLPAPER_CHOOSER = "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER";
field public static final java.lang.String COMMAND_DROP = "android.home.drop";
field public static final java.lang.String COMMAND_SECONDARY_TAP = "android.wallpaper.secondaryTap";

View File

@@ -16,8 +16,11 @@
package android.app;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -30,7 +33,7 @@ import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Binder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -41,13 +44,13 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Provides access to the system wallpaper. With WallpaperManager, you can
@@ -61,6 +64,15 @@ public class WallpaperManager {
private float mWallpaperXStep = -1;
private float mWallpaperYStep = -1;
/**
* Activity Action: Show settings for choosing wallpaper. Do not use directly to construct
* an intent; instead, use {@link #getCropAndSetWallpaperIntent}.
* <p>Input: {@link Intent#getData} is the URI of the image to crop and set as wallpaper.
* <p>Output: RESULT_OK if user decided to crop/set the wallpaper, RESULT_CANCEL otherwise
*/
public static final String ACTION_CROP_AND_SET_WALLPAPER =
"android.service.wallpaper.CROP_AND_SET_WALLPAPER";
/**
* Launch an activity for the user to pick the current global live
* wallpaper.
@@ -463,7 +475,39 @@ public class WallpaperManager {
return null;
}
}
/**
* Gets an Intent that will launch an activity that crops the given
* image and sets the device's wallpaper. If there is a default HOME activity
* that supports cropping wallpapers, it will be preferred as the default.
* Use this method instead of directly creating a {@link Intent#CROP_AND_SET_WALLPAPER}
* intent.
*/
public Intent getCropAndSetWallpaperIntent(Uri imageUri) {
final PackageManager packageManager = mContext.getPackageManager();
Intent cropAndSetWallpaperIntent =
new Intent(ACTION_CROP_AND_SET_WALLPAPER, imageUri);
cropAndSetWallpaperIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Find out if the default HOME activity supports CROP_AND_SET_WALLPAPER
Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
ResolveInfo resolvedHome = packageManager.resolveActivity(homeIntent,
PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedHome != null) {
cropAndSetWallpaperIntent.setPackage(resolvedHome.activityInfo.packageName);
List<ResolveInfo> cropAppList = packageManager.queryIntentActivities(
cropAndSetWallpaperIntent, 0);
if (cropAppList.size() > 0) {
return cropAndSetWallpaperIntent;
}
}
// fallback crop activity
cropAndSetWallpaperIntent.setPackage("com.android.wallpapercropper");
return cropAndSetWallpaperIntent;
}
/**
* Change the current system wallpaper to the bitmap in the given resource.
* The resource is opened as a raw data stream and copied into the

View File

@@ -0,0 +1,19 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := telephony-common
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
LOCAL_PACKAGE_NAME := WallpaperCropper
LOCAL_CERTIFICATE := platform
LOCAL_PRIVILEGED_MODULE := true
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -0,0 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.wallpapercropper" >
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
<application android:requiredForAllUsers="true">
<activity
android:name="WallpaperCropActivity"
android:theme="@style/Theme.WallpaperCropper"
android:label="@string/crop_wallpaper"
android:finishOnCloseSystemDialogs="true">
<intent-filter>
<action android:name="android.service.wallpaper.CROP_AND_SET_WALLPAPER" />
<data android:mimeType="image/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2013, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:actionButtonStyle"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView style="?android:actionBarTabTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingRight="20dp"
android:drawableLeft="@drawable/ic_actionbar_accept"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/wallpaper_instructions" />
</FrameLayout>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2013, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/wallpaper_cropper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.wallpapercropper.CropView
android:id="@+id/cropView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/loading"
style="@android:style/Widget.Holo.ProgressBar.Large"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateOnly="true"
android:background="@android:color/transparent" />
</RelativeLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crop_wallpaper">Crop wallpaper</string>
<!-- Button label on Wallpaper picker screen; user selects this button to set a specific wallpaper -->
<string name="wallpaper_instructions">Set wallpaper</string>
</resources>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<style name="Theme.WallpaperCropper" parent="@android:style/Theme.Holo">
<item name="android:actionBarStyle">@style/WallpaperCropperActionBar</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowActionBarOverlay">true</item>
</style>
<style name="WallpaperCropperActionBar" parent="android:style/Widget.Holo.ActionBar">
<item name="android:displayOptions">showCustom</item>
<item name="android:background">#88000000</item>
</style>
</resources>

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2010 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.gallery3d.common;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.os.Build;
import android.util.FloatMath;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class BitmapUtils {
private static final String TAG = "BitmapUtils";
private static final int DEFAULT_JPEG_QUALITY = 90;
public static final int UNCONSTRAINED = -1;
private BitmapUtils(){}
/*
* Compute the sample size as a function of minSideLength
* and maxNumOfPixels.
* minSideLength is used to specify that minimal width or height of a
* bitmap.
* maxNumOfPixels is used to specify the maximal size in pixels that is
* tolerable in terms of memory usage.
*
* The function returns a sample size based on the constraints.
* Both size and minSideLength can be passed in as UNCONSTRAINED,
* which indicates no care of the corresponding constraint.
* The functions prefers returning a sample size that
* generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
*
* Also, the function rounds up the sample size to a power of 2 or multiple
* of 8 because BitmapFactory only honors sample size this way.
* For example, BitmapFactory downsamples an image by 2 even though the
* request is 3. So we round up the sample size to avoid OOM.
*/
public static int computeSampleSize(int width, int height,
int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(
width, height, minSideLength, maxNumOfPixels);
return initialSize <= 8
? Utils.nextPowerOf2(initialSize)
: (initialSize + 7) / 8 * 8;
}
private static int computeInitialSampleSize(int w, int h,
int minSideLength, int maxNumOfPixels) {
if (maxNumOfPixels == UNCONSTRAINED
&& minSideLength == UNCONSTRAINED) return 1;
int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
(int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels));
if (minSideLength == UNCONSTRAINED) {
return lowerBound;
} else {
int sampleSize = Math.min(w / minSideLength, h / minSideLength);
return Math.max(sampleSize, lowerBound);
}
}
// This computes a sample size which makes the longer side at least
// minSideLength long. If that's not possible, return 1.
public static int computeSampleSizeLarger(int w, int h,
int minSideLength) {
int initialSize = Math.max(w / minSideLength, h / minSideLength);
if (initialSize <= 1) return 1;
return initialSize <= 8
? Utils.prevPowerOf2(initialSize)
: initialSize / 8 * 8;
}
// Find the min x that 1 / x >= scale
public static int computeSampleSizeLarger(float scale) {
int initialSize = (int) FloatMath.floor(1f / scale);
if (initialSize <= 1) return 1;
return initialSize <= 8
? Utils.prevPowerOf2(initialSize)
: initialSize / 8 * 8;
}
// Find the max x that 1 / x <= scale.
public static int computeSampleSize(float scale) {
Utils.assertTrue(scale > 0);
int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale));
return initialSize <= 8
? Utils.nextPowerOf2(initialSize)
: (initialSize + 7) / 8 * 8;
}
public static Bitmap resizeBitmapByScale(
Bitmap bitmap, float scale, boolean recycle) {
int width = Math.round(bitmap.getWidth() * scale);
int height = Math.round(bitmap.getHeight() * scale);
if (width == bitmap.getWidth()
&& height == bitmap.getHeight()) return bitmap;
Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
Canvas canvas = new Canvas(target);
canvas.scale(scale, scale);
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
canvas.drawBitmap(bitmap, 0, 0, paint);
if (recycle) bitmap.recycle();
return target;
}
private static Bitmap.Config getConfig(Bitmap bitmap) {
Bitmap.Config config = bitmap.getConfig();
if (config == null) {
config = Bitmap.Config.ARGB_8888;
}
return config;
}
public static Bitmap resizeDownBySideLength(
Bitmap bitmap, int maxLength, boolean recycle) {
int srcWidth = bitmap.getWidth();
int srcHeight = bitmap.getHeight();
float scale = Math.min(
(float) maxLength / srcWidth, (float) maxLength / srcHeight);
if (scale >= 1.0f) return bitmap;
return resizeBitmapByScale(bitmap, scale, recycle);
}
public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
if (w == size && h == size) return bitmap;
// scale the image so that the shorter side equals to the target;
// the longer side will be center-cropped.
float scale = (float) size / Math.min(w, h);
Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
int width = Math.round(scale * bitmap.getWidth());
int height = Math.round(scale * bitmap.getHeight());
Canvas canvas = new Canvas(target);
canvas.translate((size - width) / 2f, (size - height) / 2f);
canvas.scale(scale, scale);
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
canvas.drawBitmap(bitmap, 0, 0, paint);
if (recycle) bitmap.recycle();
return target;
}
public static void recycleSilently(Bitmap bitmap) {
if (bitmap == null) return;
try {
bitmap.recycle();
} catch (Throwable t) {
Log.w(TAG, "unable recycle bitmap", t);
}
}
public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
if (rotation == 0) return source;
int w = source.getWidth();
int h = source.getHeight();
Matrix m = new Matrix();
m.postRotate(rotation);
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
if (recycle) source.recycle();
return bitmap;
}
public static Bitmap createVideoThumbnail(String filePath) {
// MediaMetadataRetriever is available on API Level 8
// but is hidden until API Level 10
Class<?> clazz = null;
Object instance = null;
try {
clazz = Class.forName("android.media.MediaMetadataRetriever");
instance = clazz.newInstance();
Method method = clazz.getMethod("setDataSource", String.class);
method.invoke(instance, filePath);
// The method name changes between API Level 9 and 10.
if (Build.VERSION.SDK_INT <= 9) {
return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
} else {
byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
if (data != null) {
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap != null) return bitmap;
}
return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
}
} catch (IllegalArgumentException ex) {
// Assume this is a corrupt video file
} catch (RuntimeException ex) {
// Assume this is a corrupt video file.
} catch (InstantiationException e) {
Log.e(TAG, "createVideoThumbnail", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "createVideoThumbnail", e);
} catch (ClassNotFoundException e) {
Log.e(TAG, "createVideoThumbnail", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "createVideoThumbnail", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "createVideoThumbnail", e);
} finally {
try {
if (instance != null) {
clazz.getMethod("release").invoke(instance);
}
} catch (Exception ignored) {
}
}
return null;
}
public static byte[] compressToBytes(Bitmap bitmap) {
return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
}
public static byte[] compressToBytes(Bitmap bitmap, int quality) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
bitmap.compress(CompressFormat.JPEG, quality, baos);
return baos.toByteArray();
}
public static boolean isSupportedByRegionDecoder(String mimeType) {
if (mimeType == null) return false;
mimeType = mimeType.toLowerCase();
return mimeType.startsWith("image/") &&
(!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
}
public static boolean isRotationSupported(String mimeType) {
if (mimeType == null) return false;
mimeType = mimeType.toLowerCase();
return mimeType.equals("image/jpeg");
}
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright (C) 2010 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.gallery3d.common;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InterruptedIOException;
public class Utils {
private static final String TAG = "Utils";
private static final String DEBUG_TAG = "GalleryDebug";
private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
private static long[] sCrcTable = new long[256];
private static final boolean IS_DEBUG_BUILD =
Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug");
private static final String MASK_STRING = "********************************";
// Throws AssertionError if the input is false.
public static void assertTrue(boolean cond) {
if (!cond) {
throw new AssertionError();
}
}
// Throws AssertionError with the message. We had a method having the form
// assertTrue(boolean cond, String message, Object ... args);
// However a call to that method will cause memory allocation even if the
// condition is false (due to autoboxing generated by "Object ... args"),
// so we don't use that anymore.
public static void fail(String message, Object ... args) {
throw new AssertionError(
args.length == 0 ? message : String.format(message, args));
}
// Throws NullPointerException if the input is null.
public static <T> T checkNotNull(T object) {
if (object == null) throw new NullPointerException();
return object;
}
// Returns true if two input Object are both null or equal
// to each other.
public static boolean equals(Object a, Object b) {
return (a == b) || (a == null ? false : a.equals(b));
}
// Returns the next power of two.
// Returns the input if it is already power of 2.
// Throws IllegalArgumentException if the input is <= 0 or
// the answer overflows.
public static int nextPowerOf2(int n) {
if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n);
n -= 1;
n |= n >> 16;
n |= n >> 8;
n |= n >> 4;
n |= n >> 2;
n |= n >> 1;
return n + 1;
}
// Returns the previous power of two.
// Returns the input if it is already power of 2.
// Throws IllegalArgumentException if the input is <= 0
public static int prevPowerOf2(int n) {
if (n <= 0) throw new IllegalArgumentException();
return Integer.highestOneBit(n);
}
// Returns the input value x clamped to the range [min, max].
public static int clamp(int x, int min, int max) {
if (x > max) return max;
if (x < min) return min;
return x;
}
// Returns the input value x clamped to the range [min, max].
public static float clamp(float x, float min, float max) {
if (x > max) return max;
if (x < min) return min;
return x;
}
// Returns the input value x clamped to the range [min, max].
public static long clamp(long x, long min, long max) {
if (x > max) return max;
if (x < min) return min;
return x;
}
public static boolean isOpaque(int color) {
return color >>> 24 == 0xFF;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/**
* A function thats returns a 64-bit crc for string
*
* @param in input string
* @return a 64-bit crc value
*/
public static final long crc64Long(String in) {
if (in == null || in.length() == 0) {
return 0;
}
return crc64Long(getBytes(in));
}
static {
// http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
long part;
for (int i = 0; i < 256; i++) {
part = i;
for (int j = 0; j < 8; j++) {
long x = ((int) part & 1) != 0 ? POLY64REV : 0;
part = (part >> 1) ^ x;
}
sCrcTable[i] = part;
}
}
public static final long crc64Long(byte[] buffer) {
long crc = INITIALCRC;
for (int k = 0, n = buffer.length; k < n; ++k) {
crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8);
}
return crc;
}
public static byte[] getBytes(String in) {
byte[] result = new byte[in.length() * 2];
int output = 0;
for (char ch : in.toCharArray()) {
result[output++] = (byte) (ch & 0xFF);
result[output++] = (byte) (ch >> 8);
}
return result;
}
public static void closeSilently(Closeable c) {
if (c == null) return;
try {
c.close();
} catch (IOException t) {
Log.w(TAG, "close fail ", t);
}
}
public static int compare(long a, long b) {
return a < b ? -1 : a == b ? 0 : 1;
}
public static int ceilLog2(float value) {
int i;
for (i = 0; i < 31; i++) {
if ((1 << i) >= value) break;
}
return i;
}
public static int floorLog2(float value) {
int i;
for (i = 0; i < 31; i++) {
if ((1 << i) > value) break;
}
return i - 1;
}
public static void closeSilently(ParcelFileDescriptor fd) {
try {
if (fd != null) fd.close();
} catch (Throwable t) {
Log.w(TAG, "fail to close", t);
}
}
public static void closeSilently(Cursor cursor) {
try {
if (cursor != null) cursor.close();
} catch (Throwable t) {
Log.w(TAG, "fail to close", t);
}
}
public static float interpolateAngle(
float source, float target, float progress) {
// interpolate the angle from source to target
// We make the difference in the range of [-179, 180], this is the
// shortest path to change source to target.
float diff = target - source;
if (diff < 0) diff += 360f;
if (diff > 180) diff -= 360f;
float result = source + diff * progress;
return result < 0 ? result + 360f : result;
}
public static float interpolateScale(
float source, float target, float progress) {
return source + progress * (target - source);
}
public static String ensureNotNull(String value) {
return value == null ? "" : value;
}
public static float parseFloatSafely(String content, float defaultValue) {
if (content == null) return defaultValue;
try {
return Float.parseFloat(content);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static int parseIntSafely(String content, int defaultValue) {
if (content == null) return defaultValue;
try {
return Integer.parseInt(content);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static boolean isNullOrEmpty(String exifMake) {
return TextUtils.isEmpty(exifMake);
}
public static void waitWithoutInterrupt(Object object) {
try {
object.wait();
} catch (InterruptedException e) {
Log.w(TAG, "unexpected interrupt: " + object);
}
}
public static boolean handleInterrruptedException(Throwable e) {
// A helper to deal with the interrupt exception
// If an interrupt detected, we will setup the bit again.
if (e instanceof InterruptedIOException
|| e instanceof InterruptedException) {
Thread.currentThread().interrupt();
return true;
}
return false;
}
/**
* @return String with special XML characters escaped.
*/
public static String escapeXml(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0, len = s.length(); i < len; ++i) {
char c = s.charAt(i);
switch (c) {
case '<': sb.append("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '\"': sb.append("&quot;"); break;
case '\'': sb.append("&#039;"); break;
case '&': sb.append("&amp;"); break;
default: sb.append(c);
}
}
return sb.toString();
}
public static String getUserAgent(Context context) {
PackageInfo packageInfo;
try {
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (NameNotFoundException e) {
throw new IllegalStateException("getPackageInfo failed");
}
return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
packageInfo.packageName,
packageInfo.versionName,
Build.BRAND,
Build.DEVICE,
Build.MODEL,
Build.ID,
Build.VERSION.SDK_INT,
Build.VERSION.RELEASE,
Build.VERSION.INCREMENTAL);
}
public static String[] copyOf(String[] source, int newSize) {
String[] result = new String[newSize];
newSize = Math.min(source.length, newSize);
System.arraycopy(source, 0, result, 0, newSize);
return result;
}
// Mask information for debugging only. It returns <code>info.toString()</code> directly
// for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****")
// in release build to protect the information (e.g. for privacy issue).
public static String maskDebugInfo(Object info) {
if (info == null) return null;
String s = info.toString();
int length = Math.min(s.length(), MASK_STRING.length());
return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length);
}
// This method should be ONLY used for debugging.
public static void debug(String message, Object ... args) {
Log.v(DEBUG_TAG, String.format(message, args));
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import java.io.InputStream;
import java.nio.ByteBuffer;
class ByteBufferInputStream extends InputStream {
private ByteBuffer mBuf;
public ByteBufferInputStream(ByteBuffer buf) {
mBuf = buf;
}
@Override
public int read() {
if (!mBuf.hasRemaining()) {
return -1;
}
return mBuf.get() & 0xFF;
}
@Override
public int read(byte[] bytes, int off, int len) {
if (!mBuf.hasRemaining()) {
return -1;
}
len = Math.min(len, mBuf.remaining());
mBuf.get(bytes, off, len);
return len;
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
class CountedDataInputStream extends FilterInputStream {
private int mCount = 0;
// allocate a byte buffer for a long value;
private final byte mByteArray[] = new byte[8];
private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
protected CountedDataInputStream(InputStream in) {
super(in);
}
public int getReadByteCount() {
return mCount;
}
@Override
public int read(byte[] b) throws IOException {
int r = in.read(b);
mCount += (r >= 0) ? r : 0;
return r;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int r = in.read(b, off, len);
mCount += (r >= 0) ? r : 0;
return r;
}
@Override
public int read() throws IOException {
int r = in.read();
mCount += (r >= 0) ? 1 : 0;
return r;
}
@Override
public long skip(long length) throws IOException {
long skip = in.skip(length);
mCount += skip;
return skip;
}
public void skipOrThrow(long length) throws IOException {
if (skip(length) != length) throw new EOFException();
}
public void skipTo(long target) throws IOException {
long cur = mCount;
long diff = target - cur;
assert(diff >= 0);
skipOrThrow(diff);
}
public void readOrThrow(byte[] b, int off, int len) throws IOException {
int r = read(b, off, len);
if (r != len) throw new EOFException();
}
public void readOrThrow(byte[] b) throws IOException {
readOrThrow(b, 0, b.length);
}
public void setByteOrder(ByteOrder order) {
mByteBuffer.order(order);
}
public ByteOrder getByteOrder() {
return mByteBuffer.order();
}
public short readShort() throws IOException {
readOrThrow(mByteArray, 0 ,2);
mByteBuffer.rewind();
return mByteBuffer.getShort();
}
public int readUnsignedShort() throws IOException {
return readShort() & 0xffff;
}
public int readInt() throws IOException {
readOrThrow(mByteArray, 0 , 4);
mByteBuffer.rewind();
return mByteBuffer.getInt();
}
public long readUnsignedInt() throws IOException {
return readInt() & 0xffffffffL;
}
public long readLong() throws IOException {
readOrThrow(mByteArray, 0 , 8);
mByteBuffer.rewind();
return mByteBuffer.getLong();
}
public String readString(int n) throws IOException {
byte buf[] = new byte[n];
readOrThrow(buf);
return new String(buf, "UTF8");
}
public String readString(int n, Charset charset) throws IOException {
byte buf[] = new byte[n];
readOrThrow(buf);
return new String(buf, charset);
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class stores the EXIF header in IFDs according to the JPEG
* specification. It is the result produced by {@link ExifReader}.
*
* @see ExifReader
* @see IfdData
*/
class ExifData {
private static final String TAG = "ExifData";
private static final byte[] USER_COMMENT_ASCII = {
0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
};
private static final byte[] USER_COMMENT_JIS = {
0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final byte[] USER_COMMENT_UNICODE = {
0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
};
private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
private byte[] mThumbnail;
private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
private final ByteOrder mByteOrder;
ExifData(ByteOrder order) {
mByteOrder = order;
}
/**
* Gets the compressed thumbnail. Returns null if there is no compressed
* thumbnail.
*
* @see #hasCompressedThumbnail()
*/
protected byte[] getCompressedThumbnail() {
return mThumbnail;
}
/**
* Sets the compressed thumbnail.
*/
protected void setCompressedThumbnail(byte[] thumbnail) {
mThumbnail = thumbnail;
}
/**
* Returns true it this header contains a compressed thumbnail.
*/
protected boolean hasCompressedThumbnail() {
return mThumbnail != null;
}
/**
* Adds an uncompressed strip.
*/
protected void setStripBytes(int index, byte[] strip) {
if (index < mStripBytes.size()) {
mStripBytes.set(index, strip);
} else {
for (int i = mStripBytes.size(); i < index; i++) {
mStripBytes.add(null);
}
mStripBytes.add(strip);
}
}
/**
* Gets the strip count.
*/
protected int getStripCount() {
return mStripBytes.size();
}
/**
* Gets the strip at the specified index.
*
* @exceptions #IndexOutOfBoundException
*/
protected byte[] getStrip(int index) {
return mStripBytes.get(index);
}
/**
* Returns true if this header contains uncompressed strip.
*/
protected boolean hasUncompressedStrip() {
return mStripBytes.size() != 0;
}
/**
* Gets the byte order.
*/
protected ByteOrder getByteOrder() {
return mByteOrder;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD if it
* exists or null.
*/
protected IfdData getIfdData(int ifdId) {
if (ExifTag.isValidIfd(ifdId)) {
return mIfdDatas[ifdId];
}
return null;
}
/**
* Adds IFD data. If IFD data of the same type already exists, it will be
* replaced by the new data.
*/
protected void addIfdData(IfdData data) {
mIfdDatas[data.getId()] = data;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD or
* generates one if none exist.
*/
protected IfdData getOrCreateIfdData(int ifdId) {
IfdData ifdData = mIfdDatas[ifdId];
if (ifdData == null) {
ifdData = new IfdData(ifdId);
mIfdDatas[ifdId] = ifdData;
}
return ifdData;
}
/**
* Returns the tag with a given TID in the given IFD if the tag exists.
* Otherwise returns null.
*/
protected ExifTag getTag(short tag, int ifd) {
IfdData ifdData = mIfdDatas[ifd];
return (ifdData == null) ? null : ifdData.getTag(tag);
}
/**
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag(ExifTag tag) {
if (tag != null) {
int ifd = tag.getIfd();
return addTag(tag, ifd);
}
return null;
}
/**
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag(ExifTag tag, int ifdId) {
if (tag != null && ExifTag.isValidIfd(ifdId)) {
IfdData ifdData = getOrCreateIfdData(ifdId);
return ifdData.setTag(tag);
}
return null;
}
protected void clearThumbnailAndStrips() {
mThumbnail = null;
mStripBytes.clear();
}
/**
* Removes the thumbnail and its related tags. IFD1 will be removed.
*/
protected void removeThumbnailData() {
clearThumbnailAndStrips();
mIfdDatas[IfdId.TYPE_IFD_1] = null;
}
/**
* Removes the tag with a given TID and IFD.
*/
protected void removeTag(short tagId, int ifdId) {
IfdData ifdData = mIfdDatas[ifdId];
if (ifdData == null) {
return;
}
ifdData.removeTag(tagId);
}
/**
* Decodes the user comment tag into string as specified in the EXIF
* standard. Returns null if decoding failed.
*/
protected String getUserComment() {
IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
if (ifdData == null) {
return null;
}
ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
if (tag == null) {
return null;
}
if (tag.getComponentCount() < 8) {
return null;
}
byte[] buf = new byte[tag.getComponentCount()];
tag.getBytes(buf);
byte[] code = new byte[8];
System.arraycopy(buf, 0, code, 0, 8);
try {
if (Arrays.equals(code, USER_COMMENT_ASCII)) {
return new String(buf, 8, buf.length - 8, "US-ASCII");
} else if (Arrays.equals(code, USER_COMMENT_JIS)) {
return new String(buf, 8, buf.length - 8, "EUC-JP");
} else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
return new String(buf, 8, buf.length - 8, "UTF-16");
} else {
return null;
}
} catch (UnsupportedEncodingException e) {
Log.w(TAG, "Failed to decode the user comment");
return null;
}
}
/**
* Returns a list of all {@link ExifTag}s in the ExifData or null if there
* are none.
*/
protected List<ExifTag> getAllTags() {
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
for (IfdData d : mIfdDatas) {
if (d != null) {
ExifTag[] tags = d.getAllTags();
if (tags != null) {
for (ExifTag t : tags) {
ret.add(t);
}
}
}
}
if (ret.size() == 0) {
return null;
}
return ret;
}
/**
* Returns a list of all {@link ExifTag}s in a given IFD or null if there
* are none.
*/
protected List<ExifTag> getAllTagsForIfd(int ifd) {
IfdData d = mIfdDatas[ifd];
if (d == null) {
return null;
}
ExifTag[] tags = d.getAllTags();
if (tags == null) {
return null;
}
ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
for (ExifTag t : tags) {
ret.add(t);
}
if (ret.size() == 0) {
return null;
}
return ret;
}
/**
* Returns a list of all {@link ExifTag}s with a given TID or null if there
* are none.
*/
protected List<ExifTag> getAllTagsForTagId(short tag) {
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
for (IfdData d : mIfdDatas) {
if (d != null) {
ExifTag t = d.getTag(tag);
if (t != null) {
ret.add(t);
}
}
}
if (ret.size() == 0) {
return null;
}
return ret;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof ExifData) {
ExifData data = (ExifData) obj;
if (data.mByteOrder != mByteOrder ||
data.mStripBytes.size() != mStripBytes.size() ||
!Arrays.equals(data.mThumbnail, mThumbnail)) {
return false;
}
for (int i = 0; i < mStripBytes.size(); i++) {
if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
return false;
}
}
for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
IfdData ifd1 = data.getIfdData(i);
IfdData ifd2 = getIfdData(i);
if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
return false;
}
}
return true;
}
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
public class ExifInvalidFormatException extends Exception {
public ExifInvalidFormatException(String meg) {
super(meg);
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
class ExifModifier {
public static final String TAG = "ExifModifier";
public static final boolean DEBUG = false;
private final ByteBuffer mByteBuffer;
private final ExifData mTagToModified;
private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>();
private final ExifInterface mInterface;
private int mOffsetBase;
private static class TagOffset {
final int mOffset;
final ExifTag mTag;
TagOffset(ExifTag tag, int offset) {
mTag = tag;
mOffset = offset;
}
}
protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException,
ExifInvalidFormatException {
mByteBuffer = byteBuffer;
mOffsetBase = byteBuffer.position();
mInterface = iRef;
InputStream is = null;
try {
is = new ByteBufferInputStream(byteBuffer);
// Do not require any IFD;
ExifParser parser = ExifParser.parse(is, mInterface);
mTagToModified = new ExifData(parser.getByteOrder());
mOffsetBase += parser.getTiffStartPosition();
mByteBuffer.position(0);
} finally {
ExifInterface.closeSilently(is);
}
}
protected ByteOrder getByteOrder() {
return mTagToModified.getByteOrder();
}
protected boolean commit() throws IOException, ExifInvalidFormatException {
InputStream is = null;
try {
is = new ByteBufferInputStream(mByteBuffer);
int flag = 0;
IfdData[] ifdDatas = new IfdData[] {
mTagToModified.getIfdData(IfdId.TYPE_IFD_0),
mTagToModified.getIfdData(IfdId.TYPE_IFD_1),
mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF),
mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY),
mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS)
};
if (ifdDatas[IfdId.TYPE_IFD_0] != null) {
flag |= ExifParser.OPTION_IFD_0;
}
if (ifdDatas[IfdId.TYPE_IFD_1] != null) {
flag |= ExifParser.OPTION_IFD_1;
}
if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) {
flag |= ExifParser.OPTION_IFD_EXIF;
}
if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) {
flag |= ExifParser.OPTION_IFD_GPS;
}
if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) {
flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
}
ExifParser parser = ExifParser.parse(is, flag, mInterface);
int event = parser.next();
IfdData currIfd = null;
while (event != ExifParser.EVENT_END) {
switch (event) {
case ExifParser.EVENT_START_OF_IFD:
currIfd = ifdDatas[parser.getCurrentIfd()];
if (currIfd == null) {
parser.skipRemainingTagsInCurrentIfd();
}
break;
case ExifParser.EVENT_NEW_TAG:
ExifTag oldTag = parser.getTag();
ExifTag newTag = currIfd.getTag(oldTag.getTagId());
if (newTag != null) {
if (newTag.getComponentCount() != oldTag.getComponentCount()
|| newTag.getDataType() != oldTag.getDataType()) {
return false;
} else {
mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset()));
currIfd.removeTag(oldTag.getTagId());
if (currIfd.getTagCount() == 0) {
parser.skipRemainingTagsInCurrentIfd();
}
}
}
break;
}
event = parser.next();
}
for (IfdData ifd : ifdDatas) {
if (ifd != null && ifd.getTagCount() > 0) {
return false;
}
}
modify();
} finally {
ExifInterface.closeSilently(is);
}
return true;
}
private void modify() {
mByteBuffer.order(getByteOrder());
for (TagOffset tagOffset : mTagOffsets) {
writeTagValue(tagOffset.mTag, tagOffset.mOffset);
}
}
private void writeTagValue(ExifTag tag, int offset) {
if (DEBUG) {
Log.v(TAG, "modifying tag to: \n" + tag.toString());
Log.v(TAG, "at offset: " + offset);
}
mByteBuffer.position(offset + mOffsetBase);
switch (tag.getDataType()) {
case ExifTag.TYPE_ASCII:
byte buf[] = tag.getStringByte();
if (buf.length == tag.getComponentCount()) {
buf[buf.length - 1] = 0;
mByteBuffer.put(buf);
} else {
mByteBuffer.put(buf);
mByteBuffer.put((byte) 0);
}
break;
case ExifTag.TYPE_LONG:
case ExifTag.TYPE_UNSIGNED_LONG:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
mByteBuffer.putInt((int) tag.getValueAt(i));
}
break;
case ExifTag.TYPE_RATIONAL:
case ExifTag.TYPE_UNSIGNED_RATIONAL:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
Rational v = tag.getRational(i);
mByteBuffer.putInt((int) v.getNumerator());
mByteBuffer.putInt((int) v.getDenominator());
}
break;
case ExifTag.TYPE_UNDEFINED:
case ExifTag.TYPE_UNSIGNED_BYTE:
buf = new byte[tag.getComponentCount()];
tag.getBytes(buf);
mByteBuffer.put(buf);
break;
case ExifTag.TYPE_UNSIGNED_SHORT:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
mByteBuffer.putShort((short) tag.getValueAt(i));
}
break;
}
}
public void modifyTag(ExifTag tag) {
mTagToModified.addTag(tag);
}
}

View File

@@ -0,0 +1,518 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
/**
* This class provides a way to replace the Exif header of a JPEG image.
* <p>
* Below is an example of writing EXIF data into a file
*
* <pre>
* public static void writeExif(byte[] jpeg, ExifData exif, String path) {
* OutputStream os = null;
* try {
* os = new FileOutputStream(path);
* ExifOutputStream eos = new ExifOutputStream(os);
* // Set the exif header
* eos.setExifData(exif);
* // Write the original jpeg out, the header will be add into the file.
* eos.write(jpeg);
* } catch (FileNotFoundException e) {
* e.printStackTrace();
* } catch (IOException e) {
* e.printStackTrace();
* } finally {
* if (os != null) {
* try {
* os.close();
* } catch (IOException e) {
* e.printStackTrace();
* }
* }
* }
* }
* </pre>
*/
class ExifOutputStream extends FilterOutputStream {
private static final String TAG = "ExifOutputStream";
private static final boolean DEBUG = false;
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
private static final int STATE_SOI = 0;
private static final int STATE_FRAME_HEADER = 1;
private static final int STATE_JPEG_DATA = 2;
private static final int EXIF_HEADER = 0x45786966;
private static final short TIFF_HEADER = 0x002A;
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
private static final short TIFF_LITTLE_ENDIAN = 0x4949;
private static final short TAG_SIZE = 12;
private static final short TIFF_HEADER_SIZE = 8;
private static final int MAX_EXIF_SIZE = 65535;
private ExifData mExifData;
private int mState = STATE_SOI;
private int mByteToSkip;
private int mByteToCopy;
private byte[] mSingleByteArray = new byte[1];
private ByteBuffer mBuffer = ByteBuffer.allocate(4);
private final ExifInterface mInterface;
protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
mInterface = iRef;
}
/**
* Sets the ExifData to be written into the JPEG file. Should be called
* before writing image data.
*/
protected void setExifData(ExifData exifData) {
mExifData = exifData;
}
/**
* Gets the Exif header to be written into the JPEF file.
*/
protected ExifData getExifData() {
return mExifData;
}
private int requestByteToBuffer(int requestByteCount, byte[] buffer
, int offset, int length) {
int byteNeeded = requestByteCount - mBuffer.position();
int byteToRead = length > byteNeeded ? byteNeeded : length;
mBuffer.put(buffer, offset, byteToRead);
return byteToRead;
}
/**
* Writes the image out. The input data should be a valid JPEG format. After
* writing, it's Exif header will be replaced by the given header.
*/
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
&& length > 0) {
if (mByteToSkip > 0) {
int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
length -= byteToProcess;
mByteToSkip -= byteToProcess;
offset += byteToProcess;
}
if (mByteToCopy > 0) {
int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
out.write(buffer, offset, byteToProcess);
length -= byteToProcess;
mByteToCopy -= byteToProcess;
offset += byteToProcess;
}
if (length == 0) {
return;
}
switch (mState) {
case STATE_SOI:
int byteRead = requestByteToBuffer(2, buffer, offset, length);
offset += byteRead;
length -= byteRead;
if (mBuffer.position() < 2) {
return;
}
mBuffer.rewind();
if (mBuffer.getShort() != JpegHeader.SOI) {
throw new IOException("Not a valid jpeg image, cannot write exif");
}
out.write(mBuffer.array(), 0, 2);
mState = STATE_FRAME_HEADER;
mBuffer.rewind();
writeExifData();
break;
case STATE_FRAME_HEADER:
// We ignore the APP1 segment and copy all other segments
// until SOF tag.
byteRead = requestByteToBuffer(4, buffer, offset, length);
offset += byteRead;
length -= byteRead;
// Check if this image data doesn't contain SOF.
if (mBuffer.position() == 2) {
short tag = mBuffer.getShort();
if (tag == JpegHeader.EOI) {
out.write(mBuffer.array(), 0, 2);
mBuffer.rewind();
}
}
if (mBuffer.position() < 4) {
return;
}
mBuffer.rewind();
short marker = mBuffer.getShort();
if (marker == JpegHeader.APP1) {
mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
mState = STATE_JPEG_DATA;
} else if (!JpegHeader.isSofMarker(marker)) {
out.write(mBuffer.array(), 0, 4);
mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
} else {
out.write(mBuffer.array(), 0, 4);
mState = STATE_JPEG_DATA;
}
mBuffer.rewind();
}
}
if (length > 0) {
out.write(buffer, offset, length);
}
}
/**
* Writes the one bytes out. The input data should be a valid JPEG format.
* After writing, it's Exif header will be replaced by the given header.
*/
@Override
public void write(int oneByte) throws IOException {
mSingleByteArray[0] = (byte) (0xff & oneByte);
write(mSingleByteArray);
}
/**
* Equivalent to calling write(buffer, 0, buffer.length).
*/
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
private void writeExifData() throws IOException {
if (mExifData == null) {
return;
}
if (DEBUG) {
Log.v(TAG, "Writing exif data...");
}
ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
createRequiredIfdAndTag();
int exifSize = calculateAllOffset();
if (exifSize + 8 > MAX_EXIF_SIZE) {
throw new IOException("Exif header is too large (>64Kb)");
}
OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
dataOutputStream.writeShort(JpegHeader.APP1);
dataOutputStream.writeShort((short) (exifSize + 8));
dataOutputStream.writeInt(EXIF_HEADER);
dataOutputStream.writeShort((short) 0x0000);
if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
} else {
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
}
dataOutputStream.setByteOrder(mExifData.getByteOrder());
dataOutputStream.writeShort(TIFF_HEADER);
dataOutputStream.writeInt(8);
writeAllTags(dataOutputStream);
writeThumbnail(dataOutputStream);
for (ExifTag t : nullTags) {
mExifData.addTag(t);
}
}
private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
for(ExifTag t : data.getAllTags()) {
if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
data.removeTag(t.getTagId(), t.getIfd());
nullTags.add(t);
}
}
return nullTags;
}
private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
if (mExifData.hasCompressedThumbnail()) {
dataOutputStream.write(mExifData.getCompressedThumbnail());
} else if (mExifData.hasUncompressedStrip()) {
for (int i = 0; i < mExifData.getStripCount(); i++) {
dataOutputStream.write(mExifData.getStrip(i));
}
}
}
private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
if (interoperabilityIfd != null) {
writeIfd(interoperabilityIfd, dataOutputStream);
}
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
if (gpsIfd != null) {
writeIfd(gpsIfd, dataOutputStream);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
if (ifd1 != null) {
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
}
}
private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
throws IOException {
ExifTag[] tags = ifd.getAllTags();
dataOutputStream.writeShort((short) tags.length);
for (ExifTag tag : tags) {
dataOutputStream.writeShort(tag.getTagId());
dataOutputStream.writeShort(tag.getDataType());
dataOutputStream.writeInt(tag.getComponentCount());
if (DEBUG) {
Log.v(TAG, "\n" + tag.toString());
}
if (tag.getDataSize() > 4) {
dataOutputStream.writeInt(tag.getOffset());
} else {
ExifOutputStream.writeTagValue(tag, dataOutputStream);
for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
dataOutputStream.write(0);
}
}
}
dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
for (ExifTag tag : tags) {
if (tag.getDataSize() > 4) {
ExifOutputStream.writeTagValue(tag, dataOutputStream);
}
}
}
private int calculateOffsetOfIfd(IfdData ifd, int offset) {
offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
ExifTag[] tags = ifd.getAllTags();
for (ExifTag tag : tags) {
if (tag.getDataSize() > 4) {
tag.setOffset(offset);
offset += tag.getDataSize();
}
}
return offset;
}
private void createRequiredIfdAndTag() throws IOException {
// IFD0 is required for all file
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
if (ifd0 == null) {
ifd0 = new IfdData(IfdId.TYPE_IFD_0);
mExifData.addIfdData(ifd0);
}
ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
if (exifOffsetTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_EXIF_IFD);
}
ifd0.setTag(exifOffsetTag);
// Exif IFD is required for all files.
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
if (exifIfd == null) {
exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
mExifData.addIfdData(exifIfd);
}
// GPS IFD
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
if (gpsIfd != null) {
ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
if (gpsOffsetTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_GPS_IFD);
}
ifd0.setTag(gpsOffsetTag);
}
// Interoperability IFD
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
if (interIfd != null) {
ExifTag interOffsetTag = mInterface
.buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
if (interOffsetTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_INTEROPERABILITY_IFD);
}
exifIfd.setTag(interOffsetTag);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
// thumbnail
if (mExifData.hasCompressedThumbnail()) {
if (ifd1 == null) {
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
mExifData.addIfdData(ifd1);
}
ExifTag offsetTag = mInterface
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
if (offsetTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
}
ifd1.setTag(offsetTag);
ExifTag lengthTag = mInterface
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
if (lengthTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
}
lengthTag.setValue(mExifData.getCompressedThumbnail().length);
ifd1.setTag(lengthTag);
// Get rid of tags for uncompressed if they exist.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
} else if (mExifData.hasUncompressedStrip()) {
if (ifd1 == null) {
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
mExifData.addIfdData(ifd1);
}
int stripCount = mExifData.getStripCount();
ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
if (offsetTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_STRIP_OFFSETS);
}
ExifTag lengthTag = mInterface
.buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
if (lengthTag == null) {
throw new IOException("No definition for crucial exif tag: "
+ ExifInterface.TAG_STRIP_BYTE_COUNTS);
}
long[] lengths = new long[stripCount];
for (int i = 0; i < mExifData.getStripCount(); i++) {
lengths[i] = mExifData.getStrip(i).length;
}
lengthTag.setValue(lengths);
ifd1.setTag(offsetTag);
ifd1.setTag(lengthTag);
// Get rid of tags for compressed if they exist.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
ifd1.removeTag(ExifInterface
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
} else if (ifd1 != null) {
// Get rid of offset and length tags if there is no thumbnail.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
ifd1.removeTag(ExifInterface
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
}
}
private int calculateAllOffset() {
int offset = TIFF_HEADER_SIZE;
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
offset = calculateOffsetOfIfd(ifd0, offset);
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
offset = calculateOffsetOfIfd(exifIfd, offset);
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
if (interIfd != null) {
exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
.setValue(offset);
offset = calculateOffsetOfIfd(interIfd, offset);
}
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
if (gpsIfd != null) {
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
offset = calculateOffsetOfIfd(gpsIfd, offset);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
if (ifd1 != null) {
ifd0.setOffsetToNextIfd(offset);
offset = calculateOffsetOfIfd(ifd1, offset);
}
// thumbnail
if (mExifData.hasCompressedThumbnail()) {
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
.setValue(offset);
offset += mExifData.getCompressedThumbnail().length;
} else if (mExifData.hasUncompressedStrip()) {
int stripCount = mExifData.getStripCount();
long[] offsets = new long[stripCount];
for (int i = 0; i < mExifData.getStripCount(); i++) {
offsets[i] = offset;
offset += mExifData.getStrip(i).length;
}
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
offsets);
}
return offset;
}
static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
throws IOException {
switch (tag.getDataType()) {
case ExifTag.TYPE_ASCII:
byte buf[] = tag.getStringByte();
if (buf.length == tag.getComponentCount()) {
buf[buf.length - 1] = 0;
dataOutputStream.write(buf);
} else {
dataOutputStream.write(buf);
dataOutputStream.write(0);
}
break;
case ExifTag.TYPE_LONG:
case ExifTag.TYPE_UNSIGNED_LONG:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeInt((int) tag.getValueAt(i));
}
break;
case ExifTag.TYPE_RATIONAL:
case ExifTag.TYPE_UNSIGNED_RATIONAL:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeRational(tag.getRational(i));
}
break;
case ExifTag.TYPE_UNDEFINED:
case ExifTag.TYPE_UNSIGNED_BYTE:
buf = new byte[tag.getComponentCount()];
tag.getBytes(buf);
dataOutputStream.write(buf);
break;
case ExifTag.TYPE_UNSIGNED_SHORT:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeShort((short) tag.getValueAt(i));
}
break;
}
}
}

View File

@@ -0,0 +1,916 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.Map.Entry;
import java.util.TreeMap;
/**
* This class provides a low-level EXIF parsing API. Given a JPEG format
* InputStream, the caller can request which IFD's to read via
* {@link #parse(InputStream, int)} with given options.
* <p>
* Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the
* parser.
*
* <pre>
* void parse() {
* ExifParser parser = ExifParser.parse(mImageInputStream,
* ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
* int event = parser.next();
* while (event != ExifParser.EVENT_END) {
* switch (event) {
* case ExifParser.EVENT_START_OF_IFD:
* break;
* case ExifParser.EVENT_NEW_TAG:
* ExifTag tag = parser.getTag();
* if (!tag.hasValue()) {
* parser.registerForTagValue(tag);
* } else {
* processTag(tag);
* }
* break;
* case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
* tag = parser.getTag();
* if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
* processTag(tag);
* }
* break;
* }
* event = parser.next();
* }
* }
*
* void processTag(ExifTag tag) {
* // process the tag as you like.
* }
* </pre>
*/
class ExifParser {
private static final boolean LOGV = false;
private static final String TAG = "ExifParser";
/**
* When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to
* know which IFD we are in.
*/
public static final int EVENT_START_OF_IFD = 0;
/**
* When the parser reaches a new tag. Call {@link #getTag()}to get the
* corresponding tag.
*/
public static final int EVENT_NEW_TAG = 1;
/**
* When the parser reaches the value area of tag that is registered by
* {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()}
* to get the corresponding tag.
*/
public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
/**
* When the parser reaches the compressed image area.
*/
public static final int EVENT_COMPRESSED_IMAGE = 3;
/**
* When the parser reaches the uncompressed image strip. Call
* {@link #getStripIndex()} to get the index of the strip.
*
* @see #getStripIndex()
* @see #getStripCount()
*/
public static final int EVENT_UNCOMPRESSED_STRIP = 4;
/**
* When there is nothing more to parse.
*/
public static final int EVENT_END = 5;
/**
* Option bit to request to parse IFD0.
*/
public static final int OPTION_IFD_0 = 1 << 0;
/**
* Option bit to request to parse IFD1.
*/
public static final int OPTION_IFD_1 = 1 << 1;
/**
* Option bit to request to parse Exif-IFD.
*/
public static final int OPTION_IFD_EXIF = 1 << 2;
/**
* Option bit to request to parse GPS-IFD.
*/
public static final int OPTION_IFD_GPS = 1 << 3;
/**
* Option bit to request to parse Interoperability-IFD.
*/
public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
/**
* Option bit to request to parse thumbnail.
*/
public static final int OPTION_THUMBNAIL = 1 << 5;
protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
// TIFF header
protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
protected static final short TIFF_HEADER_TAIL = 0x002A;
protected static final int TAG_SIZE = 12;
protected static final int OFFSET_SIZE = 2;
private static final Charset US_ASCII = Charset.forName("US-ASCII");
protected static final int DEFAULT_IFD0_OFFSET = 8;
private final CountedDataInputStream mTiffStream;
private final int mOptions;
private int mIfdStartOffset = 0;
private int mNumOfTagInIfd = 0;
private int mIfdType;
private ExifTag mTag;
private ImageEvent mImageEvent;
private int mStripCount;
private ExifTag mStripSizeTag;
private ExifTag mJpegSizeTag;
private boolean mNeedToParseOffsetsInCurrentIfd;
private boolean mContainExifData = false;
private int mApp1End;
private int mOffsetToApp1EndFromSOF = 0;
private byte[] mDataAboveIfd0;
private int mIfd0Position;
private int mTiffStartPosition;
private final ExifInterface mInterface;
private static final short TAG_EXIF_IFD = ExifInterface
.getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
private static final short TAG_INTEROPERABILITY_IFD = ExifInterface
.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
private static final short TAG_STRIP_OFFSETS = ExifInterface
.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface
.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
private boolean isIfdRequested(int ifdType) {
switch (ifdType) {
case IfdId.TYPE_IFD_0:
return (mOptions & OPTION_IFD_0) != 0;
case IfdId.TYPE_IFD_1:
return (mOptions & OPTION_IFD_1) != 0;
case IfdId.TYPE_IFD_EXIF:
return (mOptions & OPTION_IFD_EXIF) != 0;
case IfdId.TYPE_IFD_GPS:
return (mOptions & OPTION_IFD_GPS) != 0;
case IfdId.TYPE_IFD_INTEROPERABILITY:
return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
}
return false;
}
private boolean isThumbnailRequested() {
return (mOptions & OPTION_THUMBNAIL) != 0;
}
private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
throws IOException, ExifInvalidFormatException {
if (inputStream == null) {
throw new IOException("Null argument inputStream to ExifParser");
}
if (LOGV) {
Log.v(TAG, "Reading exif...");
}
mInterface = iRef;
mContainExifData = seekTiffData(inputStream);
mTiffStream = new CountedDataInputStream(inputStream);
mOptions = options;
if (!mContainExifData) {
return;
}
parseTiffHeader();
long offset = mTiffStream.readUnsignedInt();
if (offset > Integer.MAX_VALUE) {
throw new ExifInvalidFormatException("Invalid offset " + offset);
}
mIfd0Position = (int) offset;
mIfdType = IfdId.TYPE_IFD_0;
if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
registerIfd(IfdId.TYPE_IFD_0, offset);
if (offset != DEFAULT_IFD0_OFFSET) {
mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
read(mDataAboveIfd0);
}
}
}
/**
* Parses the the given InputStream with the given options
*
* @exception IOException
* @exception ExifInvalidFormatException
*/
protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
throws IOException, ExifInvalidFormatException {
return new ExifParser(inputStream, options, iRef);
}
/**
* Parses the the given InputStream with default options; that is, every IFD
* and thumbnaill will be parsed.
*
* @exception IOException
* @exception ExifInvalidFormatException
* @see #parse(InputStream, int)
*/
protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
throws IOException, ExifInvalidFormatException {
return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1
| OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY
| OPTION_THUMBNAIL, iRef);
}
/**
* Moves the parser forward and returns the next parsing event
*
* @exception IOException
* @exception ExifInvalidFormatException
* @see #EVENT_START_OF_IFD
* @see #EVENT_NEW_TAG
* @see #EVENT_VALUE_OF_REGISTERED_TAG
* @see #EVENT_COMPRESSED_IMAGE
* @see #EVENT_UNCOMPRESSED_STRIP
* @see #EVENT_END
*/
protected int next() throws IOException, ExifInvalidFormatException {
if (!mContainExifData) {
return EVENT_END;
}
int offset = mTiffStream.getReadByteCount();
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
if (offset < endOfTags) {
mTag = readTag();
if (mTag == null) {
return next();
}
if (mNeedToParseOffsetsInCurrentIfd) {
checkOffsetOrImageTag(mTag);
}
return EVENT_NEW_TAG;
} else if (offset == endOfTags) {
// There is a link to ifd1 at the end of ifd0
if (mIfdType == IfdId.TYPE_IFD_0) {
long ifdOffset = readUnsignedLong();
if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
if (ifdOffset != 0) {
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
}
}
} else {
int offsetSize = 4;
// Some camera models use invalid length of the offset
if (mCorrespondingEvent.size() > 0) {
offsetSize = mCorrespondingEvent.firstEntry().getKey() -
mTiffStream.getReadByteCount();
}
if (offsetSize < 4) {
Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize);
} else {
long ifdOffset = readUnsignedLong();
if (ifdOffset != 0) {
Log.w(TAG, "Invalid link to next IFD: " + ifdOffset);
}
}
}
}
while (mCorrespondingEvent.size() != 0) {
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
Object event = entry.getValue();
try {
skipTo(entry.getKey());
} catch (IOException e) {
Log.w(TAG, "Failed to skip to data at: " + entry.getKey() +
" for " + event.getClass().getName() + ", the file may be broken.");
continue;
}
if (event instanceof IfdEvent) {
mIfdType = ((IfdEvent) event).ifd;
mNumOfTagInIfd = mTiffStream.readUnsignedShort();
mIfdStartOffset = entry.getKey();
if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
Log.w(TAG, "Invalid size of IFD " + mIfdType);
return EVENT_END;
}
mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
if (((IfdEvent) event).isRequested) {
return EVENT_START_OF_IFD;
} else {
skipRemainingTagsInCurrentIfd();
}
} else if (event instanceof ImageEvent) {
mImageEvent = (ImageEvent) event;
return mImageEvent.type;
} else {
ExifTagEvent tagEvent = (ExifTagEvent) event;
mTag = tagEvent.tag;
if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
readFullTagValue(mTag);
checkOffsetOrImageTag(mTag);
}
if (tagEvent.isRequested) {
return EVENT_VALUE_OF_REGISTERED_TAG;
}
}
}
return EVENT_END;
}
/**
* Skips the tags area of current IFD, if the parser is not in the tag area,
* nothing will happen.
*
* @throws IOException
* @throws ExifInvalidFormatException
*/
protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
int offset = mTiffStream.getReadByteCount();
if (offset > endOfTags) {
return;
}
if (mNeedToParseOffsetsInCurrentIfd) {
while (offset < endOfTags) {
mTag = readTag();
offset += TAG_SIZE;
if (mTag == null) {
continue;
}
checkOffsetOrImageTag(mTag);
}
} else {
skipTo(endOfTags);
}
long ifdOffset = readUnsignedLong();
// For ifd0, there is a link to ifd1 in the end of all tags
if (mIfdType == IfdId.TYPE_IFD_0
&& (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
if (ifdOffset > 0) {
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
}
}
}
private boolean needToParseOffsetsInCurrentIfd() {
switch (mIfdType) {
case IfdId.TYPE_IFD_0:
return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS)
|| isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
|| isIfdRequested(IfdId.TYPE_IFD_1);
case IfdId.TYPE_IFD_1:
return isThumbnailRequested();
case IfdId.TYPE_IFD_EXIF:
// The offset to interoperability IFD is located in Exif IFD
return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
default:
return false;
}
}
/**
* If {@link #next()} return {@link #EVENT_NEW_TAG} or
* {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the
* corresponding tag.
* <p>
* For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size
* of the value is greater than 4 bytes. One should call
* {@link ExifTag#hasValue()} to check if the tag contains value. If there
* is no value,call {@link #registerForTagValue(ExifTag)} to have the parser
* emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
* pointed by the offset.
* <p>
* When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the
* tag will have already been read except for tags of undefined type. For
* tags of undefined type, call one of the read methods to get the value.
*
* @see #registerForTagValue(ExifTag)
* @see #read(byte[])
* @see #read(byte[], int, int)
* @see #readLong()
* @see #readRational()
* @see #readString(int)
* @see #readString(int, Charset)
*/
protected ExifTag getTag() {
return mTag;
}
/**
* Gets number of tags in the current IFD area.
*/
protected int getTagCountInCurrentIfd() {
return mNumOfTagInIfd;
}
/**
* Gets the ID of current IFD.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
* @see IfdId#TYPE_IFD_EXIF
*/
protected int getCurrentIfd() {
return mIfdType;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the index of this strip.
*
* @see #getStripCount()
*/
protected int getStripIndex() {
return mImageEvent.stripIndex;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the number of strip data.
*
* @see #getStripIndex()
*/
protected int getStripCount() {
return mStripCount;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the strip size.
*/
protected int getStripSize() {
if (mStripSizeTag == null)
return 0;
return (int) mStripSizeTag.getValueAt(0);
}
/**
* When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get
* the image data size.
*/
protected int getCompressedImageSize() {
if (mJpegSizeTag == null) {
return 0;
}
return (int) mJpegSizeTag.getValueAt(0);
}
private void skipTo(int offset) throws IOException {
mTiffStream.skipTo(offset);
while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
mCorrespondingEvent.pollFirstEntry();
}
}
/**
* When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may
* not contain the value if the size of the value is greater than 4 bytes.
* When the value is not available here, call this method so that the parser
* will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
* where the value is located.
*
* @see #EVENT_VALUE_OF_REGISTERED_TAG
*/
protected void registerForTagValue(ExifTag tag) {
if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
}
}
private void registerIfd(int ifdType, long offset) {
// Cast unsigned int to int since the offset is always smaller
// than the size of APP1 (65536)
mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
}
private void registerCompressedImage(long offset) {
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
}
private void registerUncompressedStrip(int stripIndex, long offset) {
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP
, stripIndex));
}
private ExifTag readTag() throws IOException, ExifInvalidFormatException {
short tagId = mTiffStream.readShort();
short dataFormat = mTiffStream.readShort();
long numOfComp = mTiffStream.readUnsignedInt();
if (numOfComp > Integer.MAX_VALUE) {
throw new ExifInvalidFormatException(
"Number of component is larger then Integer.MAX_VALUE");
}
// Some invalid image file contains invalid data type. Ignore those tags
if (!ExifTag.isValidType(dataFormat)) {
Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat));
mTiffStream.skip(4);
return null;
}
// TODO: handle numOfComp overflow
ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType,
((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
int dataSize = tag.getDataSize();
if (dataSize > 4) {
long offset = mTiffStream.readUnsignedInt();
if (offset > Integer.MAX_VALUE) {
throw new ExifInvalidFormatException(
"offset is larger then Integer.MAX_VALUE");
}
// Some invalid images put some undefined data before IFD0.
// Read the data here.
if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
byte[] buf = new byte[(int) numOfComp];
System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET,
buf, 0, (int) numOfComp);
tag.setValue(buf);
} else {
tag.setOffset((int) offset);
}
} else {
boolean defCount = tag.hasDefinedCount();
// Set defined count to 0 so we can add \0 to non-terminated strings
tag.setHasDefinedCount(false);
// Read value
readFullTagValue(tag);
tag.setHasDefinedCount(defCount);
mTiffStream.skip(4 - dataSize);
// Set the offset to the position of value.
tag.setOffset(mTiffStream.getReadByteCount() - 4);
}
return tag;
}
/**
* Check the tag, if the tag is one of the offset tag that points to the IFD
* or image the caller is interested in, register the IFD or image.
*/
private void checkOffsetOrImageTag(ExifTag tag) {
// Some invalid formattd image contains tag with 0 size.
if (tag.getComponentCount() == 0) {
return;
}
short tid = tag.getTagId();
int ifd = tag.getIfd();
if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
|| isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
}
} else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
}
} else if (tid == TAG_INTEROPERABILITY_IFD
&& checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
}
} else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
&& checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
if (isThumbnailRequested()) {
registerCompressedImage(tag.getValueAt(0));
}
} else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
&& checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
if (isThumbnailRequested()) {
mJpegSizeTag = tag;
}
} else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
if (isThumbnailRequested()) {
if (tag.hasValue()) {
for (int i = 0; i < tag.getComponentCount(); i++) {
if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
registerUncompressedStrip(i, tag.getValueAt(i));
} else {
registerUncompressedStrip(i, tag.getValueAt(i));
}
}
} else {
mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
}
}
} else if (tid == TAG_STRIP_BYTE_COUNTS
&& checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
&&isThumbnailRequested() && tag.hasValue()) {
mStripSizeTag = tag;
}
}
private boolean checkAllowed(int ifd, int tagId) {
int info = mInterface.getTagInfo().get(tagId);
if (info == ExifInterface.DEFINITION_NULL) {
return false;
}
return ExifInterface.isIfdAllowed(info, ifd);
}
protected void readFullTagValue(ExifTag tag) throws IOException {
// Some invalid images contains tags with wrong size, check it here
short type = tag.getDataType();
if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
type == ExifTag.TYPE_UNSIGNED_BYTE) {
int size = tag.getComponentCount();
if (mCorrespondingEvent.size() > 0) {
if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount()
+ size) {
Object event = mCorrespondingEvent.firstEntry().getValue();
if (event instanceof ImageEvent) {
// Tag value overlaps thumbnail, ignore thumbnail.
Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString());
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
} else {
// Tag value overlaps another tag, shorten count
if (event instanceof IfdEvent) {
Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd
+ " overlaps value for tag: \n" + tag.toString());
} else if (event instanceof ExifTagEvent) {
Log.w(TAG, "Tag value for tag: \n"
+ ((ExifTagEvent) event).tag.toString()
+ " overlaps value for tag: \n" + tag.toString());
}
size = mCorrespondingEvent.firstEntry().getKey()
- mTiffStream.getReadByteCount();
Log.w(TAG, "Invalid size of tag: \n" + tag.toString()
+ " setting count to: " + size);
tag.forceSetComponentCount(size);
}
}
}
}
switch (tag.getDataType()) {
case ExifTag.TYPE_UNSIGNED_BYTE:
case ExifTag.TYPE_UNDEFINED: {
byte buf[] = new byte[tag.getComponentCount()];
read(buf);
tag.setValue(buf);
}
break;
case ExifTag.TYPE_ASCII:
tag.setValue(readString(tag.getComponentCount()));
break;
case ExifTag.TYPE_UNSIGNED_LONG: {
long value[] = new long[tag.getComponentCount()];
for (int i = 0, n = value.length; i < n; i++) {
value[i] = readUnsignedLong();
}
tag.setValue(value);
}
break;
case ExifTag.TYPE_UNSIGNED_RATIONAL: {
Rational value[] = new Rational[tag.getComponentCount()];
for (int i = 0, n = value.length; i < n; i++) {
value[i] = readUnsignedRational();
}
tag.setValue(value);
}
break;
case ExifTag.TYPE_UNSIGNED_SHORT: {
int value[] = new int[tag.getComponentCount()];
for (int i = 0, n = value.length; i < n; i++) {
value[i] = readUnsignedShort();
}
tag.setValue(value);
}
break;
case ExifTag.TYPE_LONG: {
int value[] = new int[tag.getComponentCount()];
for (int i = 0, n = value.length; i < n; i++) {
value[i] = readLong();
}
tag.setValue(value);
}
break;
case ExifTag.TYPE_RATIONAL: {
Rational value[] = new Rational[tag.getComponentCount()];
for (int i = 0, n = value.length; i < n; i++) {
value[i] = readRational();
}
tag.setValue(value);
}
break;
}
if (LOGV) {
Log.v(TAG, "\n" + tag.toString());
}
}
private void parseTiffHeader() throws IOException,
ExifInvalidFormatException {
short byteOrder = mTiffStream.readShort();
if (LITTLE_ENDIAN_TAG == byteOrder) {
mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
} else if (BIG_ENDIAN_TAG == byteOrder) {
mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
} else {
throw new ExifInvalidFormatException("Invalid TIFF header");
}
if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
throw new ExifInvalidFormatException("Invalid TIFF header");
}
}
private boolean seekTiffData(InputStream inputStream) throws IOException,
ExifInvalidFormatException {
CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
if (dataStream.readShort() != JpegHeader.SOI) {
throw new ExifInvalidFormatException("Invalid JPEG format");
}
short marker = dataStream.readShort();
while (marker != JpegHeader.EOI
&& !JpegHeader.isSofMarker(marker)) {
int length = dataStream.readUnsignedShort();
// Some invalid formatted image contains multiple APP1,
// try to find the one with Exif data.
if (marker == JpegHeader.APP1) {
int header = 0;
short headerTail = 0;
if (length >= 8) {
header = dataStream.readInt();
headerTail = dataStream.readShort();
length -= 6;
if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
mTiffStartPosition = dataStream.getReadByteCount();
mApp1End = length;
mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
return true;
}
}
}
if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
Log.w(TAG, "Invalid JPEG format.");
return false;
}
marker = dataStream.readShort();
}
return false;
}
protected int getOffsetToExifEndFromSOF() {
return mOffsetToApp1EndFromSOF;
}
protected int getTiffStartPosition() {
return mTiffStartPosition;
}
/**
* Reads bytes from the InputStream.
*/
protected int read(byte[] buffer, int offset, int length) throws IOException {
return mTiffStream.read(buffer, offset, length);
}
/**
* Equivalent to read(buffer, 0, buffer.length).
*/
protected int read(byte[] buffer) throws IOException {
return mTiffStream.read(buffer);
}
/**
* Reads a String from the InputStream with US-ASCII charset. The parser
* will read n bytes and convert it to ascii string. This is used for
* reading values of type {@link ExifTag#TYPE_ASCII}.
*/
protected String readString(int n) throws IOException {
return readString(n, US_ASCII);
}
/**
* Reads a String from the InputStream with the given charset. The parser
* will read n bytes and convert it to string. This is used for reading
* values of type {@link ExifTag#TYPE_ASCII}.
*/
protected String readString(int n, Charset charset) throws IOException {
if (n > 0) {
return mTiffStream.readString(n, charset);
} else {
return "";
}
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
* InputStream.
*/
protected int readUnsignedShort() throws IOException {
return mTiffStream.readShort() & 0xffff;
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
* InputStream.
*/
protected long readUnsignedLong() throws IOException {
return readLong() & 0xffffffffL;
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
* InputStream.
*/
protected Rational readUnsignedRational() throws IOException {
long nomi = readUnsignedLong();
long denomi = readUnsignedLong();
return new Rational(nomi, denomi);
}
/**
* Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
*/
protected int readLong() throws IOException {
return mTiffStream.readInt();
}
/**
* Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
*/
protected Rational readRational() throws IOException {
int nomi = readLong();
int denomi = readLong();
return new Rational(nomi, denomi);
}
private static class ImageEvent {
int stripIndex;
int type;
ImageEvent(int type) {
this.stripIndex = 0;
this.type = type;
}
ImageEvent(int type, int stripIndex) {
this.type = type;
this.stripIndex = stripIndex;
}
}
private static class IfdEvent {
int ifd;
boolean isRequested;
IfdEvent(int ifd, boolean isInterestedIfd) {
this.ifd = ifd;
this.isRequested = isInterestedIfd;
}
}
private static class ExifTagEvent {
ExifTag tag;
boolean isRequested;
ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
this.tag = tag;
this.isRequested = isRequireByUser;
}
}
/**
* Gets the byte order of the current InputStream.
*/
protected ByteOrder getByteOrder() {
return mTiffStream.getByteOrder();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* This class reads the EXIF header of a JPEG file and stores it in
* {@link ExifData}.
*/
class ExifReader {
private static final String TAG = "ExifReader";
private final ExifInterface mInterface;
ExifReader(ExifInterface iRef) {
mInterface = iRef;
}
/**
* Parses the inputStream and and returns the EXIF data in an
* {@link ExifData}.
*
* @throws ExifInvalidFormatException
* @throws IOException
*/
protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
IOException {
ExifParser parser = ExifParser.parse(inputStream, mInterface);
ExifData exifData = new ExifData(parser.getByteOrder());
ExifTag tag = null;
int event = parser.next();
while (event != ExifParser.EVENT_END) {
switch (event) {
case ExifParser.EVENT_START_OF_IFD:
exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
break;
case ExifParser.EVENT_NEW_TAG:
tag = parser.getTag();
if (!tag.hasValue()) {
parser.registerForTagValue(tag);
} else {
exifData.getIfdData(tag.getIfd()).setTag(tag);
}
break;
case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
tag = parser.getTag();
if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
parser.readFullTagValue(tag);
}
exifData.getIfdData(tag.getIfd()).setTag(tag);
break;
case ExifParser.EVENT_COMPRESSED_IMAGE:
byte buf[] = new byte[parser.getCompressedImageSize()];
if (buf.length == parser.read(buf)) {
exifData.setCompressedThumbnail(buf);
} else {
Log.w(TAG, "Failed to read the compressed thumbnail");
}
break;
case ExifParser.EVENT_UNCOMPRESSED_STRIP:
buf = new byte[parser.getStripSize()];
if (buf.length == parser.read(buf)) {
exifData.setStripBytes(parser.getStripIndex(), buf);
} else {
Log.w(TAG, "Failed to read the strip bytes");
}
break;
}
event = parser.next();
}
return exifData;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import java.util.HashMap;
import java.util.Map;
/**
* This class stores all the tags in an IFD.
*
* @see ExifData
* @see ExifTag
*/
class IfdData {
private final int mIfdId;
private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
private int mOffsetToNextIfd = 0;
private static final int[] sIfds = {
IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF,
IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS
};
/**
* Creates an IfdData with given IFD ID.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
IfdData(int ifdId) {
mIfdId = ifdId;
}
static protected int[] getIfds() {
return sIfds;
}
/**
* Get a array the contains all {@link ExifTag} in this IFD.
*/
protected ExifTag[] getAllTags() {
return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
}
/**
* Gets the ID of this IFD.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
protected int getId() {
return mIfdId;
}
/**
* Gets the {@link ExifTag} with given tag id. Return null if there is no
* such tag.
*/
protected ExifTag getTag(short tagId) {
return mExifTags.get(tagId);
}
/**
* Adds or replaces a {@link ExifTag}.
*/
protected ExifTag setTag(ExifTag tag) {
tag.setIfd(mIfdId);
return mExifTags.put(tag.getTagId(), tag);
}
protected boolean checkCollision(short tagId) {
return mExifTags.get(tagId) != null;
}
/**
* Removes the tag of the given ID
*/
protected void removeTag(short tagId) {
mExifTags.remove(tagId);
}
/**
* Gets the tags count in the IFD.
*/
protected int getTagCount() {
return mExifTags.size();
}
/**
* Sets the offset of next IFD.
*/
protected void setOffsetToNextIfd(int offset) {
mOffsetToNextIfd = offset;
}
/**
* Gets the offset of next IFD.
*/
protected int getOffsetToNextIfd() {
return mOffsetToNextIfd;
}
/**
* Returns true if all tags in this two IFDs are equal. Note that tags of
* IFDs offset or thumbnail offset will be ignored.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof IfdData) {
IfdData data = (IfdData) obj;
if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
ExifTag[] tags = data.getAllTags();
for (ExifTag tag : tags) {
if (ExifInterface.isOffsetTag(tag.getTagId())) {
continue;
}
ExifTag tag2 = mExifTags.get(tag.getTagId());
if (!tag.equals(tag2)) {
return false;
}
}
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
/**
* The constants of the IFD ID defined in EXIF spec.
*/
public interface IfdId {
public static final int TYPE_IFD_0 = 0;
public static final int TYPE_IFD_1 = 1;
public static final int TYPE_IFD_EXIF = 2;
public static final int TYPE_IFD_INTEROPERABILITY = 3;
public static final int TYPE_IFD_GPS = 4;
/* This is used in ExifData to allocate enough IfdData */
static final int TYPE_IFD_COUNT = 5;
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
class JpegHeader {
public static final short SOI = (short) 0xFFD8;
public static final short APP1 = (short) 0xFFE1;
public static final short APP0 = (short) 0xFFE0;
public static final short EOI = (short) 0xFFD9;
/**
* SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG,
* and DAC marker.
*/
public static final short SOF0 = (short) 0xFFC0;
public static final short SOF15 = (short) 0xFFCF;
public static final short DHT = (short) 0xFFC4;
public static final short JPG = (short) 0xFFC8;
public static final short DAC = (short) 0xFFCC;
public static final boolean isSofMarker(short marker) {
return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
&& marker != DAC;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class OrderedDataOutputStream extends FilterOutputStream {
private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
public OrderedDataOutputStream(OutputStream out) {
super(out);
}
public OrderedDataOutputStream setByteOrder(ByteOrder order) {
mByteBuffer.order(order);
return this;
}
public OrderedDataOutputStream writeShort(short value) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putShort(value);
out.write(mByteBuffer.array(), 0, 2);
return this;
}
public OrderedDataOutputStream writeInt(int value) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putInt(value);
out.write(mByteBuffer.array());
return this;
}
public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
writeInt((int) rational.getNumerator());
writeInt((int) rational.getDenominator());
return this;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.exif;
/**
* The rational data type of EXIF tag. Contains a pair of longs representing the
* numerator and denominator of a Rational number.
*/
public class Rational {
private final long mNumerator;
private final long mDenominator;
/**
* Create a Rational with a given numerator and denominator.
*
* @param nominator
* @param denominator
*/
public Rational(long nominator, long denominator) {
mNumerator = nominator;
mDenominator = denominator;
}
/**
* Create a copy of a Rational.
*/
public Rational(Rational r) {
mNumerator = r.mNumerator;
mDenominator = r.mDenominator;
}
/**
* Gets the numerator of the rational.
*/
public long getNumerator() {
return mNumerator;
}
/**
* Gets the denominator of the rational
*/
public long getDenominator() {
return mDenominator;
}
/**
* Gets the rational value as type double. Will cause a divide-by-zero error
* if the denominator is 0.
*/
public double toDouble() {
return mNumerator / (double) mDenominator;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj instanceof Rational) {
Rational data = (Rational) obj;
return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
}
return false;
}
@Override
public String toString() {
return mNumerator + "/" + mDenominator;
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import android.util.Log;
import com.android.gallery3d.common.Utils;
import java.util.WeakHashMap;
// BasicTexture is a Texture corresponds to a real GL texture.
// The state of a BasicTexture indicates whether its data is loaded to GL memory.
// If a BasicTexture is loaded into GL memory, it has a GL texture id.
public abstract class BasicTexture implements Texture {
@SuppressWarnings("unused")
private static final String TAG = "BasicTexture";
protected static final int UNSPECIFIED = -1;
protected static final int STATE_UNLOADED = 0;
protected static final int STATE_LOADED = 1;
protected static final int STATE_ERROR = -1;
// Log a warning if a texture is larger along a dimension
private static final int MAX_TEXTURE_SIZE = 4096;
protected int mId = -1;
protected int mState;
protected int mWidth = UNSPECIFIED;
protected int mHeight = UNSPECIFIED;
protected int mTextureWidth;
protected int mTextureHeight;
private boolean mHasBorder;
protected GLCanvas mCanvasRef = null;
private static WeakHashMap<BasicTexture, Object> sAllTextures
= new WeakHashMap<BasicTexture, Object>();
private static ThreadLocal sInFinalizer = new ThreadLocal();
protected BasicTexture(GLCanvas canvas, int id, int state) {
setAssociatedCanvas(canvas);
mId = id;
mState = state;
synchronized (sAllTextures) {
sAllTextures.put(this, null);
}
}
protected BasicTexture() {
this(null, 0, STATE_UNLOADED);
}
protected void setAssociatedCanvas(GLCanvas canvas) {
mCanvasRef = canvas;
}
/**
* Sets the content size of this texture. In OpenGL, the actual texture
* size must be of power of 2, the size of the content may be smaller.
*/
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
mTextureWidth = width > 0 ? Utils.nextPowerOf2(width) : 0;
mTextureHeight = height > 0 ? Utils.nextPowerOf2(height) : 0;
if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
Log.w(TAG, String.format("texture is too large: %d x %d",
mTextureWidth, mTextureHeight), new Exception());
}
}
public boolean isFlippedVertically() {
return false;
}
public int getId() {
return mId;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
// Returns the width rounded to the next power of 2.
public int getTextureWidth() {
return mTextureWidth;
}
// Returns the height rounded to the next power of 2.
public int getTextureHeight() {
return mTextureHeight;
}
// Returns true if the texture has one pixel transparent border around the
// actual content. This is used to avoid jigged edges.
//
// The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
// mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
// covered by the texture will use the color of the edge texel. If we add
// the transparent border, the color of the edge texel will be mixed with
// appropriate amount of transparent.
//
// Currently our background is black, so we can draw the thumbnails without
// enabling blending.
public boolean hasBorder() {
return mHasBorder;
}
protected void setBorder(boolean hasBorder) {
mHasBorder = hasBorder;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
canvas.drawTexture(this, x, y, getWidth(), getHeight());
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
canvas.drawTexture(this, x, y, w, h);
}
// onBind is called before GLCanvas binds this texture.
// It should make sure the data is uploaded to GL memory.
abstract protected boolean onBind(GLCanvas canvas);
// Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
abstract protected int getTarget();
public boolean isLoaded() {
return mState == STATE_LOADED;
}
// recycle() is called when the texture will never be used again,
// so it can free all resources.
public void recycle() {
freeResource();
}
// yield() is called when the texture will not be used temporarily,
// so it can free some resources.
// The default implementation unloads the texture from GL memory, so
// the subclass should make sure it can reload the texture to GL memory
// later, or it will have to override this method.
public void yield() {
freeResource();
}
private void freeResource() {
GLCanvas canvas = mCanvasRef;
if (canvas != null && mId != -1) {
canvas.unloadTexture(this);
mId = -1; // Don't free it again.
}
mState = STATE_UNLOADED;
setAssociatedCanvas(null);
}
@Override
protected void finalize() {
sInFinalizer.set(BasicTexture.class);
recycle();
sInFinalizer.set(null);
}
// This is for deciding if we can call Bitmap's recycle().
// We cannot call Bitmap's recycle() in finalizer because at that point
// the finalizer of Bitmap may already be called so recycle() will crash.
public static boolean inFinalizer() {
return sInFinalizer.get() != null;
}
public static void yieldAllTextures() {
synchronized (sAllTextures) {
for (BasicTexture t : sAllTextures.keySet()) {
t.yield();
}
}
}
public static void invalidateAllTextures() {
synchronized (sAllTextures) {
for (BasicTexture t : sAllTextures.keySet()) {
t.mState = STATE_UNLOADED;
t.setAssociatedCanvas(null);
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import android.graphics.Bitmap;
import junit.framework.Assert;
// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
//
// The texture does not own the Bitmap. The user should make sure the Bitmap
// is valid during the texture's lifetime. When the texture is recycled, it
// does not free the Bitmap.
public class BitmapTexture extends UploadedTexture {
protected Bitmap mContentBitmap;
public BitmapTexture(Bitmap bitmap) {
this(bitmap, false);
}
public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
super(hasBorder);
Assert.assertTrue(bitmap != null && !bitmap.isRecycled());
mContentBitmap = bitmap;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
// Do nothing.
}
@Override
protected Bitmap onGetBitmap() {
return mContentBitmap;
}
public Bitmap getBitmap() {
return mContentBitmap;
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import javax.microedition.khronos.opengles.GL11;
//
// GLCanvas gives a convenient interface to draw using OpenGL.
//
// When a rectangle is specified in this interface, it means the region
// [x, x+width) * [y, y+height)
//
public interface GLCanvas {
public GLId getGLId();
// Tells GLCanvas the size of the underlying GL surface. This should be
// called before first drawing and when the size of GL surface is changed.
// This is called by GLRoot and should not be called by the clients
// who only want to draw on the GLCanvas. Both width and height must be
// nonnegative.
public abstract void setSize(int width, int height);
// Clear the drawing buffers. This should only be used by GLRoot.
public abstract void clearBuffer();
public abstract void clearBuffer(float[] argb);
// Sets and gets the current alpha, alpha must be in [0, 1].
public abstract void setAlpha(float alpha);
public abstract float getAlpha();
// (current alpha) = (current alpha) * alpha
public abstract void multiplyAlpha(float alpha);
// Change the current transform matrix.
public abstract void translate(float x, float y, float z);
public abstract void translate(float x, float y);
public abstract void scale(float sx, float sy, float sz);
public abstract void rotate(float angle, float x, float y, float z);
public abstract void multiplyMatrix(float[] mMatrix, int offset);
// Pushes the configuration state (matrix, and alpha) onto
// a private stack.
public abstract void save();
// Same as save(), but only save those specified in saveFlags.
public abstract void save(int saveFlags);
public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
public static final int SAVE_FLAG_ALPHA = 0x01;
public static final int SAVE_FLAG_MATRIX = 0x02;
// Pops from the top of the stack as current configuration state (matrix,
// alpha, and clip). This call balances a previous call to save(), and is
// used to remove all modifications to the configuration state since the
// last save call.
public abstract void restore();
// Draws a line using the specified paint from (x1, y1) to (x2, y2).
// (Both end points are included).
public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
// Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
// (Both end points are included).
public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
// Fills the specified rectangle with the specified color.
public abstract void fillRect(float x, float y, float width, float height, int color);
// Draws a texture to the specified rectangle.
public abstract void drawTexture(
BasicTexture texture, int x, int y, int width, int height);
public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
int uvBuffer, int indexBuffer, int indexCount);
// Draws the source rectangle part of the texture to the target rectangle.
public abstract void drawTexture(BasicTexture texture, RectF source, RectF target);
// Draw a texture with a specified texture transform.
public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform,
int x, int y, int w, int h);
// Draw two textures to the specified rectangle. The actual texture used is
// from * (1 - ratio) + to * ratio
// The two textures must have the same size.
public abstract void drawMixed(BasicTexture from, int toColor,
float ratio, int x, int y, int w, int h);
// Draw a region of a texture and a specified color to the specified
// rectangle. The actual color used is from * (1 - ratio) + to * ratio.
// The region of the texture is defined by parameter "src". The target
// rectangle is specified by parameter "target".
public abstract void drawMixed(BasicTexture from, int toColor,
float ratio, RectF src, RectF target);
// Unloads the specified texture from the canvas. The resource allocated
// to draw the texture will be released. The specified texture will return
// to the unloaded state. This function should be called only from
// BasicTexture or its descendant
public abstract boolean unloadTexture(BasicTexture texture);
// Delete the specified buffer object, similar to unloadTexture.
public abstract void deleteBuffer(int bufferId);
// Delete the textures and buffers in GL side. This function should only be
// called in the GL thread.
public abstract void deleteRecycledResources();
// Dump statistics information and clear the counters. For debug only.
public abstract void dumpStatisticsAndClear();
public abstract void beginRenderTarget(RawTexture texture);
public abstract void endRenderTarget();
/**
* Sets texture parameters to use GL_CLAMP_TO_EDGE for both
* GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be
* GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.
* bindTexture() must be called prior to this.
*
* @param texture The texture to set parameters on.
*/
public abstract void setTextureParameters(BasicTexture texture);
/**
* Initializes the texture to a size by calling texImage2D on it.
*
* @param texture The texture to initialize the size.
* @param format The texture format (e.g. GL_RGBA)
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
*/
public abstract void initializeTextureSize(BasicTexture texture, int format, int type);
/**
* Initializes the texture to a size by calling texImage2D on it.
*
* @param texture The texture to initialize the size.
* @param bitmap The bitmap to initialize the bitmap with.
*/
public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap);
/**
* Calls glTexSubImage2D to upload a bitmap to the texture.
*
* @param texture The target texture to write to.
* @param xOffset Specifies a texel offset in the x direction within the
* texture array.
* @param yOffset Specifies a texel offset in the y direction within the
* texture array.
* @param format The texture format (e.g. GL_RGBA)
* @param type The texture type (e.g. GL_UNSIGNED_BYTE)
*/
public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
Bitmap bitmap,
int format, int type);
/**
* Generates buffers and uploads the buffer data.
*
* @param buffer The buffer to upload
* @return The buffer ID that was generated.
*/
public abstract int uploadBuffer(java.nio.FloatBuffer buffer);
/**
* Generates buffers and uploads the element array buffer data.
*
* @param buffer The buffer to upload
* @return The buffer ID that was generated.
*/
public abstract int uploadBuffer(java.nio.ByteBuffer buffer);
/**
* After LightCycle makes GL calls, this method is called to restore the GL
* configuration to the one expected by GLCanvas.
*/
public abstract void recoverFromLightCycle();
/**
* Gets the bounds given by x, y, width, and height as well as the internal
* matrix state. There is no special handling for non-90-degree rotations.
* It only considers the lower-left and upper-right corners as the bounds.
*
* @param bounds The output bounds to write to.
* @param x The left side of the input rectangle.
* @param y The bottom of the input rectangle.
* @param width The width of the input rectangle.
* @param height The height of the input rectangle.
*/
public abstract void getBounds(Rect bounds, int x, int y, int width, int height);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
package com.android.gallery3d.glrenderer;
import android.opengl.GLES20;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
public class GLES20IdImpl implements GLId {
private final int[] mTempIntArray = new int[1];
@Override
public int generateTexture() {
GLES20.glGenTextures(1, mTempIntArray, 0);
GLES20Canvas.checkError();
return mTempIntArray[0];
}
@Override
public void glGenBuffers(int n, int[] buffers, int offset) {
GLES20.glGenBuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
GLES20.glDeleteTextures(n, textures, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
GLES20.glDeleteBuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
@Override
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
GLES20.glDeleteFramebuffers(n, buffers, offset);
GLES20Canvas.checkError();
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.glrenderer;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
// This mimics corresponding GL functions.
public interface GLId {
public int generateTexture();
public void glGenBuffers(int n, int[] buffers, int offset);
public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);
public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);
public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import junit.framework.Assert;
public class GLPaint {
private float mLineWidth = 1f;
private int mColor = 0;
public void setColor(int color) {
mColor = color;
}
public int getColor() {
return mColor;
}
public void setLineWidth(float width) {
Assert.assertTrue(width >= 0);
mLineWidth = width;
}
public float getLineWidth() {
return mLineWidth;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import android.util.Log;
import javax.microedition.khronos.opengles.GL11;
public class RawTexture extends BasicTexture {
private static final String TAG = "RawTexture";
private final boolean mOpaque;
private boolean mIsFlipped;
public RawTexture(int width, int height, boolean opaque) {
mOpaque = opaque;
setSize(width, height);
}
@Override
public boolean isOpaque() {
return mOpaque;
}
@Override
public boolean isFlippedVertically() {
return mIsFlipped;
}
public void setIsFlippedVertically(boolean isFlipped) {
mIsFlipped = isFlipped;
}
protected void prepare(GLCanvas canvas) {
GLId glId = canvas.getGLId();
mId = glId.generateTexture();
canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
canvas.setTextureParameters(this);
mState = STATE_LOADED;
setAssociatedCanvas(canvas);
}
@Override
protected boolean onBind(GLCanvas canvas) {
if (isLoaded()) return true;
Log.w(TAG, "lost the content due to context change");
return false;
}
@Override
public void yield() {
// we cannot free the texture because we have no backup.
}
@Override
protected int getTarget() {
return GL11.GL_TEXTURE_2D;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
// Texture is a rectangular image which can be drawn on GLCanvas.
// The isOpaque() function gives a hint about whether the texture is opaque,
// so the drawing can be done faster.
//
// This is the current texture hierarchy:
//
// Texture
// -- ColorTexture
// -- FadeInTexture
// -- BasicTexture
// -- UploadedTexture
// -- BitmapTexture
// -- Tile
// -- ResourceTexture
// -- NinePatchTexture
// -- CanvasTexture
// -- StringTexture
//
public interface Texture {
public int getWidth();
public int getHeight();
public void draw(GLCanvas canvas, int x, int y);
public void draw(GLCanvas canvas, int x, int y, int w, int h);
public boolean isOpaque();
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (C) 2010 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.gallery3d.glrenderer;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.opengl.GLUtils;
import junit.framework.Assert;
import java.util.HashMap;
import javax.microedition.khronos.opengles.GL11;
// UploadedTextures use a Bitmap for the content of the texture.
//
// Subclasses should implement onGetBitmap() to provide the Bitmap and
// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
// is not needed anymore.
//
// isContentValid() is meaningful only when the isLoaded() returns true.
// It means whether the content needs to be updated.
//
// The user of this class should call recycle() when the texture is not
// needed anymore.
//
// By default an UploadedTexture is opaque (so it can be drawn faster without
// blending). The user or subclass can override it using setOpaque().
public abstract class UploadedTexture extends BasicTexture {
// To prevent keeping allocation the borders, we store those used borders here.
// Since the length will be power of two, it won't use too much memory.
private static HashMap<BorderKey, Bitmap> sBorderLines =
new HashMap<BorderKey, Bitmap>();
private static BorderKey sBorderKey = new BorderKey();
@SuppressWarnings("unused")
private static final String TAG = "Texture";
private boolean mContentValid = true;
// indicate this textures is being uploaded in background
private boolean mIsUploading = false;
private boolean mOpaque = true;
private boolean mThrottled = false;
private static int sUploadedCount;
private static final int UPLOAD_LIMIT = 100;
protected Bitmap mBitmap;
private int mBorder;
protected UploadedTexture() {
this(false);
}
protected UploadedTexture(boolean hasBorder) {
super(null, 0, STATE_UNLOADED);
if (hasBorder) {
setBorder(true);
mBorder = 1;
}
}
protected void setIsUploading(boolean uploading) {
mIsUploading = uploading;
}
public boolean isUploading() {
return mIsUploading;
}
private static class BorderKey implements Cloneable {
public boolean vertical;
public Config config;
public int length;
@Override
public int hashCode() {
int x = config.hashCode() ^ length;
return vertical ? x : -x;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof BorderKey)) return false;
BorderKey o = (BorderKey) object;
return vertical == o.vertical
&& config == o.config && length == o.length;
}
@Override
public BorderKey clone() {
try {
return (BorderKey) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
protected void setThrottled(boolean throttled) {
mThrottled = throttled;
}
private static Bitmap getBorderLine(
boolean vertical, Config config, int length) {
BorderKey key = sBorderKey;
key.vertical = vertical;
key.config = config;
key.length = length;
Bitmap bitmap = sBorderLines.get(key);
if (bitmap == null) {
bitmap = vertical
? Bitmap.createBitmap(1, length, config)
: Bitmap.createBitmap(length, 1, config);
sBorderLines.put(key.clone(), bitmap);
}
return bitmap;
}
private Bitmap getBitmap() {
if (mBitmap == null) {
mBitmap = onGetBitmap();
int w = mBitmap.getWidth() + mBorder * 2;
int h = mBitmap.getHeight() + mBorder * 2;
if (mWidth == UNSPECIFIED) {
setSize(w, h);
}
}
return mBitmap;
}
private void freeBitmap() {
Assert.assertTrue(mBitmap != null);
onFreeBitmap(mBitmap);
mBitmap = null;
}
@Override
public int getWidth() {
if (mWidth == UNSPECIFIED) getBitmap();
return mWidth;
}
@Override
public int getHeight() {
if (mWidth == UNSPECIFIED) getBitmap();
return mHeight;
}
protected abstract Bitmap onGetBitmap();
protected abstract void onFreeBitmap(Bitmap bitmap);
protected void invalidateContent() {
if (mBitmap != null) freeBitmap();
mContentValid = false;
mWidth = UNSPECIFIED;
mHeight = UNSPECIFIED;
}
/**
* Whether the content on GPU is valid.
*/
public boolean isContentValid() {
return isLoaded() && mContentValid;
}
/**
* Updates the content on GPU's memory.
* @param canvas
*/
public void updateContent(GLCanvas canvas) {
if (!isLoaded()) {
if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
return;
}
uploadToCanvas(canvas);
} else if (!mContentValid) {
Bitmap bitmap = getBitmap();
int format = GLUtils.getInternalFormat(bitmap);
int type = GLUtils.getType(bitmap);
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
freeBitmap();
mContentValid = true;
}
}
public static void resetUploadLimit() {
sUploadedCount = 0;
}
public static boolean uploadLimitReached() {
return sUploadedCount > UPLOAD_LIMIT;
}
private void uploadToCanvas(GLCanvas canvas) {
Bitmap bitmap = getBitmap();
if (bitmap != null) {
try {
int bWidth = bitmap.getWidth();
int bHeight = bitmap.getHeight();
int width = bWidth + mBorder * 2;
int height = bHeight + mBorder * 2;
int texWidth = getTextureWidth();
int texHeight = getTextureHeight();
Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
// Upload the bitmap to a new texture.
mId = canvas.getGLId().generateTexture();
canvas.setTextureParameters(this);
if (bWidth == texWidth && bHeight == texHeight) {
canvas.initializeTexture(this, bitmap);
} else {
int format = GLUtils.getInternalFormat(bitmap);
int type = GLUtils.getType(bitmap);
Config config = bitmap.getConfig();
canvas.initializeTextureSize(this, format, type);
canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
if (mBorder > 0) {
// Left border
Bitmap line = getBorderLine(true, config, texHeight);
canvas.texSubImage2D(this, 0, 0, line, format, type);
// Top border
line = getBorderLine(false, config, texWidth);
canvas.texSubImage2D(this, 0, 0, line, format, type);
}
// Right border
if (mBorder + bWidth < texWidth) {
Bitmap line = getBorderLine(true, config, texHeight);
canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
}
// Bottom border
if (mBorder + bHeight < texHeight) {
Bitmap line = getBorderLine(false, config, texWidth);
canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
}
}
} finally {
freeBitmap();
}
// Update texture state.
setAssociatedCanvas(canvas);
mState = STATE_LOADED;
mContentValid = true;
} else {
mState = STATE_ERROR;
throw new RuntimeException("Texture load fail, no bitmap");
}
}
@Override
protected boolean onBind(GLCanvas canvas) {
updateContent(canvas);
return isContentValid();
}
@Override
protected int getTarget() {
return GL11.GL_TEXTURE_2D;
}
public void setOpaque(boolean isOpaque) {
mOpaque = isOpaque;
}
@Override
public boolean isOpaque() {
return mOpaque;
}
@Override
public void recycle() {
super.recycle();
if (mBitmap != null) freeBitmap();
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2010 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.gallery3d.util;
public class IntArray {
private static final int INIT_CAPACITY = 8;
private int mData[] = new int[INIT_CAPACITY];
private int mSize = 0;
public void add(int value) {
if (mData.length == mSize) {
int temp[] = new int[mSize + mSize];
System.arraycopy(mData, 0, temp, 0, mSize);
mData = temp;
}
mData[mSize++] = value;
}
public int removeLast() {
mSize--;
return mData[mSize];
}
public int size() {
return mSize;
}
// For testing only
public int[] toArray(int[] result) {
if (result == null || result.length < mSize) {
result = new int[mSize];
}
System.arraycopy(mData, 0, result, 0, mSize);
return result;
}
public int[] getInternalArray() {
return mData;
}
public void clear() {
mSize = 0;
if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.photos;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.BitmapTexture;
import com.android.photos.views.TiledImageRenderer;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
* {@link BitmapRegionDecoder} to wrap a local file
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
private static final String TAG = "BitmapRegionTileSource";
private static final boolean REUSE_BITMAP =
Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
private static final int GL_SIZE_LIMIT = 2048;
// This must be no larger than half the size of the GL_SIZE_LIMIT
// due to decodePreview being allowed to be up to 2x the size of the target
private static final int MAX_PREVIEW_SIZE = 1024;
BitmapRegionDecoder mDecoder;
int mWidth;
int mHeight;
int mTileSize;
private BasicTexture mPreview;
private final int mRotation;
// For use only by getTile
private Rect mWantRegion = new Rect();
private Rect mOverlapRegion = new Rect();
private BitmapFactory.Options mOptions;
private Canvas mCanvas;
public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
this(null, context, path, null, 0, previewSize, rotation);
}
public BitmapRegionTileSource(Context context, Uri uri, int previewSize, int rotation) {
this(null, context, null, uri, 0, previewSize, rotation);
}
public BitmapRegionTileSource(Resources res,
Context context, int resId, int previewSize, int rotation) {
this(res, context, null, null, resId, previewSize, rotation);
}
private BitmapRegionTileSource(Resources res,
Context context, String path, Uri uri, int resId, int previewSize, int rotation) {
mTileSize = TiledImageRenderer.suggestedTileSize(context);
mRotation = rotation;
try {
if (path != null) {
mDecoder = BitmapRegionDecoder.newInstance(path, true);
} else if (uri != null) {
InputStream is = context.getContentResolver().openInputStream(uri);
BufferedInputStream bis = new BufferedInputStream(is);
mDecoder = BitmapRegionDecoder.newInstance(bis, true);
} else {
InputStream is = res.openRawResource(resId);
BufferedInputStream bis = new BufferedInputStream(is);
mDecoder = BitmapRegionDecoder.newInstance(bis, true);
}
mWidth = mDecoder.getWidth();
mHeight = mDecoder.getHeight();
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "ctor failed", e);
}
mOptions = new BitmapFactory.Options();
mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
mOptions.inPreferQualityOverSpeed = true;
mOptions.inTempStorage = new byte[16 * 1024];
if (previewSize != 0) {
previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
// Although this is the same size as the Bitmap that is likely already
// loaded, the lifecycle is different and interactions are on a different
// thread. Thus to simplify, this source will decode its own bitmap.
Bitmap preview = decodePreview(res, context, path, uri, resId, previewSize);
if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
mPreview = new BitmapTexture(preview);
} else {
Log.w(TAG, String.format(
"Failed to create preview of apropriate size! "
+ " in: %dx%d, out: %dx%d",
mWidth, mHeight,
preview.getWidth(), preview.getHeight()));
}
}
}
@Override
public int getTileSize() {
return mTileSize;
}
@Override
public int getImageWidth() {
return mWidth;
}
@Override
public int getImageHeight() {
return mHeight;
}
@Override
public BasicTexture getPreview() {
return mPreview;
}
@Override
public int getRotation() {
return mRotation;
}
@Override
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
int tileSize = getTileSize();
if (!REUSE_BITMAP) {
return getTileWithoutReusingBitmap(level, x, y, tileSize);
}
int t = tileSize << level;
mWantRegion.set(x, y, x + t, y + t);
if (bitmap == null) {
bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
}
mOptions.inSampleSize = (1 << level);
mOptions.inBitmap = bitmap;
try {
bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
} finally {
if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
mOptions.inBitmap = null;
}
}
if (bitmap == null) {
Log.w("BitmapRegionTileSource", "fail in decoding region");
}
return bitmap;
}
private Bitmap getTileWithoutReusingBitmap(
int level, int x, int y, int tileSize) {
int t = tileSize << level;
mWantRegion.set(x, y, x + t, y + t);
mOverlapRegion.set(0, 0, mWidth, mHeight);
mOptions.inSampleSize = (1 << level);
Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
if (bitmap == null) {
Log.w(TAG, "fail in decoding region");
}
if (mWantRegion.equals(mOverlapRegion)) {
return bitmap;
}
Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
if (mCanvas == null) {
mCanvas = new Canvas();
}
mCanvas.setBitmap(result);
mCanvas.drawBitmap(bitmap,
(mOverlapRegion.left - mWantRegion.left) >> level,
(mOverlapRegion.top - mWantRegion.top) >> level, null);
mCanvas.setBitmap(null);
return result;
}
/**
* Note that the returned bitmap may have a long edge that's longer
* than the targetSize, but it will always be less than 2x the targetSize
*/
private Bitmap decodePreview(
Resources res, Context context, String file, Uri uri, int resId, int targetSize) {
float scale = (float) targetSize / Math.max(mWidth, mHeight);
mOptions.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
mOptions.inJustDecodeBounds = false;
Bitmap result = null;
if (file != null) {
result = BitmapFactory.decodeFile(file, mOptions);
} else if (uri != null) {
try {
InputStream is = context.getContentResolver().openInputStream(uri);
BufferedInputStream bis = new BufferedInputStream(is);
result = BitmapFactory.decodeStream(bis, null, mOptions);
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "getting preview failed", e);
}
} else {
result = BitmapFactory.decodeResource(res, resId, mOptions);
}
if (result == null) {
return null;
}
// We need to resize down if the decoder does not support inSampleSize
// or didn't support the specified inSampleSize (some decoders only do powers of 2)
scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
if (scale <= 0.5) {
result = BitmapUtils.resizeBitmapByScale(result, scale, true);
}
return ensureGLCompatibleBitmap(result);
}
private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
if (bitmap == null || bitmap.getConfig() != null) {
return bitmap;
}
Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
bitmap.recycle();
return newBitmap;
}
}

View File

@@ -0,0 +1,438 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.photos.views;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLUtils;
import android.util.Log;
import android.view.TextureView;
import android.view.TextureView.SurfaceTextureListener;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
/**
* A TextureView that supports blocking rendering for synchronous drawing
*/
public class BlockingGLTextureView extends TextureView
implements SurfaceTextureListener {
private RenderThread mRenderThread;
public BlockingGLTextureView(Context context) {
super(context);
setSurfaceTextureListener(this);
}
public void setRenderer(Renderer renderer) {
if (mRenderThread != null) {
throw new IllegalArgumentException("Renderer already set");
}
mRenderThread = new RenderThread(renderer);
}
public void render() {
mRenderThread.render();
}
public void destroy() {
if (mRenderThread != null) {
mRenderThread.finish();
mRenderThread = null;
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height) {
mRenderThread.setSurface(surface);
mRenderThread.setSize(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height) {
mRenderThread.setSize(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
if (mRenderThread != null) {
mRenderThread.setSurface(null);
}
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
@Override
protected void finalize() throws Throwable {
try {
destroy();
} catch (Throwable t) {
// Ignore
}
super.finalize();
}
/**
* An EGL helper class.
*/
private static class EglHelper {
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
private static final int EGL_OPENGL_ES2_BIT = 4;
EGL10 mEgl;
EGLDisplay mEglDisplay;
EGLSurface mEglSurface;
EGLConfig mEglConfig;
EGLContext mEglContext;
private EGLConfig chooseEglConfig() {
int[] configsCount = new int[1];
EGLConfig[] configs = new EGLConfig[1];
int[] configSpec = getConfig();
if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
throw new IllegalArgumentException("eglChooseConfig failed " +
GLUtils.getEGLErrorString(mEgl.eglGetError()));
} else if (configsCount[0] > 0) {
return configs[0];
}
return null;
}
private static int[] getConfig() {
return new int[] {
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 0,
EGL10.EGL_STENCIL_SIZE, 0,
EGL10.EGL_NONE
};
}
EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
}
/**
* Initialize EGL for a given configuration spec.
*/
public void start() {
/*
* Get an EGL instance
*/
mEgl = (EGL10) EGLContext.getEGL();
/*
* Get to the default display.
*/
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}
/*
* We can now initialize EGL for that display
*/
int[] version = new int[2];
if (!mEgl.eglInitialize(mEglDisplay, version)) {
throw new RuntimeException("eglInitialize failed");
}
mEglConfig = chooseEglConfig();
/*
* Create an EGL context. We want to do this as rarely as we can, because an
* EGL context is a somewhat heavy object.
*/
mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
mEglContext = null;
throwEglException("createContext");
}
mEglSurface = null;
}
/**
* Create an egl surface for the current SurfaceTexture surface. If a surface
* already exists, destroy it before creating the new surface.
*
* @return true if the surface was created successfully.
*/
public boolean createSurface(SurfaceTexture surface) {
/*
* Check preconditions.
*/
if (mEgl == null) {
throw new RuntimeException("egl not initialized");
}
if (mEglDisplay == null) {
throw new RuntimeException("eglDisplay not initialized");
}
if (mEglConfig == null) {
throw new RuntimeException("mEglConfig not initialized");
}
/*
* The window size has changed, so we need to create a new
* surface.
*/
destroySurfaceImp();
/*
* Create an EGL surface we can render into.
*/
if (surface != null) {
mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
} else {
mEglSurface = null;
}
if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
int error = mEgl.eglGetError();
if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
}
return false;
}
/*
* Before we can issue GL commands, we need to make sure
* the context is current and bound to a surface.
*/
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
/*
* Could not make the context current, probably because the underlying
* SurfaceView surface has been destroyed.
*/
logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
return false;
}
return true;
}
/**
* Create a GL object for the current EGL context.
*/
public GL10 createGL() {
return (GL10) mEglContext.getGL();
}
/**
* Display the current render surface.
* @return the EGL error code from eglSwapBuffers.
*/
public int swap() {
if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
return mEgl.eglGetError();
}
return EGL10.EGL_SUCCESS;
}
public void destroySurface() {
destroySurfaceImp();
}
private void destroySurfaceImp() {
if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_CONTEXT);
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
}
}
public void finish() {
if (mEglContext != null) {
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
mEglContext = null;
}
if (mEglDisplay != null) {
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
}
}
private void throwEglException(String function) {
throwEglException(function, mEgl.eglGetError());
}
public static void throwEglException(String function, int error) {
String message = formatEglError(function, error);
throw new RuntimeException(message);
}
public static void logEglErrorAsWarning(String tag, String function, int error) {
Log.w(tag, formatEglError(function, error));
}
public static String formatEglError(String function, int error) {
return function + " failed: " + error;
}
}
private static class RenderThread extends Thread {
private static final int INVALID = -1;
private static final int RENDER = 1;
private static final int CHANGE_SURFACE = 2;
private static final int RESIZE_SURFACE = 3;
private static final int FINISH = 4;
private EglHelper mEglHelper = new EglHelper();
private Object mLock = new Object();
private int mExecMsgId = INVALID;
private SurfaceTexture mSurface;
private Renderer mRenderer;
private int mWidth, mHeight;
private boolean mFinished = false;
private GL10 mGL;
public RenderThread(Renderer renderer) {
super("RenderThread");
mRenderer = renderer;
start();
}
private void checkRenderer() {
if (mRenderer == null) {
throw new IllegalArgumentException("Renderer is null!");
}
}
private void checkSurface() {
if (mSurface == null) {
throw new IllegalArgumentException("surface is null!");
}
}
public void setSurface(SurfaceTexture surface) {
// If the surface is null we're being torn down, don't need a
// renderer then
if (surface != null) {
checkRenderer();
}
mSurface = surface;
exec(CHANGE_SURFACE);
}
public void setSize(int width, int height) {
checkRenderer();
checkSurface();
mWidth = width;
mHeight = height;
exec(RESIZE_SURFACE);
}
public void render() {
checkRenderer();
if (mSurface != null) {
exec(RENDER);
mSurface.updateTexImage();
}
}
public void finish() {
mSurface = null;
exec(FINISH);
try {
join();
} catch (InterruptedException e) {
// Ignore
}
}
private void exec(int msgid) {
synchronized (mLock) {
if (mExecMsgId != INVALID) {
throw new IllegalArgumentException(
"Message already set - multithreaded access?");
}
mExecMsgId = msgid;
mLock.notify();
try {
mLock.wait();
} catch (InterruptedException e) {
// Ignore
}
}
}
private void handleMessageLocked(int what) {
switch (what) {
case CHANGE_SURFACE:
if (mEglHelper.createSurface(mSurface)) {
mGL = mEglHelper.createGL();
mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
}
break;
case RESIZE_SURFACE:
mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
break;
case RENDER:
mRenderer.onDrawFrame(mGL);
mEglHelper.swap();
break;
case FINISH:
mEglHelper.destroySurface();
mEglHelper.finish();
mFinished = true;
break;
}
}
@Override
public void run() {
synchronized (mLock) {
mEglHelper.start();
while (!mFinished) {
while (mExecMsgId == INVALID) {
try {
mLock.wait();
} catch (InterruptedException e) {
// Ignore
}
}
handleMessageLocked(mExecMsgId);
mExecMsgId = INVALID;
mLock.notify();
}
mExecMsgId = FINISH;
}
}
}
}

View File

@@ -0,0 +1,825 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.photos.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v4.util.LongSparseArray;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pools.Pool;
import android.util.Pools.SynchronizedPool;
import android.view.View;
import android.view.WindowManager;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.GLCanvas;
import com.android.gallery3d.glrenderer.UploadedTexture;
/**
* Handles laying out, decoding, and drawing of tiles in GL
*/
public class TiledImageRenderer {
public static final int SIZE_UNKNOWN = -1;
private static final String TAG = "TiledImageRenderer";
private static final int UPLOAD_LIMIT = 1;
/*
* This is the tile state in the CPU side.
* Life of a Tile:
* ACTIVATED (initial state)
* --> IN_QUEUE - by queueForDecode()
* --> RECYCLED - by recycleTile()
* IN_QUEUE --> DECODING - by decodeTile()
* --> RECYCLED - by recycleTile)
* DECODING --> RECYCLING - by recycleTile()
* --> DECODED - by decodeTile()
* --> DECODE_FAIL - by decodeTile()
* RECYCLING --> RECYCLED - by decodeTile()
* DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
* DECODED --> RECYCLED - by recycleTile()
* DECODE_FAIL -> RECYCLED - by recycleTile()
* RECYCLED --> ACTIVATED - by obtainTile()
*/
private static final int STATE_ACTIVATED = 0x01;
private static final int STATE_IN_QUEUE = 0x02;
private static final int STATE_DECODING = 0x04;
private static final int STATE_DECODED = 0x08;
private static final int STATE_DECODE_FAIL = 0x10;
private static final int STATE_RECYCLING = 0x20;
private static final int STATE_RECYCLED = 0x40;
private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
// TILE_SIZE must be 2^N
private int mTileSize;
private TileSource mModel;
private BasicTexture mPreview;
protected int mLevelCount; // cache the value of mScaledBitmaps.length
// The mLevel variable indicates which level of bitmap we should use.
// Level 0 means the original full-sized bitmap, and a larger value means
// a smaller scaled bitmap (The width and height of each scaled bitmap is
// half size of the previous one). If the value is in [0, mLevelCount), we
// use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
// is mLevelCount
private int mLevel = 0;
private int mOffsetX;
private int mOffsetY;
private int mUploadQuota;
private boolean mRenderComplete;
private final RectF mSourceRect = new RectF();
private final RectF mTargetRect = new RectF();
private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
// The following three queue are guarded by mQueueLock
private final Object mQueueLock = new Object();
private final TileQueue mRecycledQueue = new TileQueue();
private final TileQueue mUploadQueue = new TileQueue();
private final TileQueue mDecodeQueue = new TileQueue();
// The width and height of the full-sized bitmap
protected int mImageWidth = SIZE_UNKNOWN;
protected int mImageHeight = SIZE_UNKNOWN;
protected int mCenterX;
protected int mCenterY;
protected float mScale;
protected int mRotation;
private boolean mLayoutTiles;
// Temp variables to avoid memory allocation
private final Rect mTileRange = new Rect();
private final Rect mActiveRange[] = {new Rect(), new Rect()};
private TileDecoder mTileDecoder;
private boolean mBackgroundTileUploaded;
private int mViewWidth, mViewHeight;
private View mParent;
/**
* Interface for providing tiles to a {@link TiledImageRenderer}
*/
public static interface TileSource {
/**
* If the source does not care about the tile size, it should use
* {@link TiledImageRenderer#suggestedTileSize(Context)}
*/
public int getTileSize();
public int getImageWidth();
public int getImageHeight();
public int getRotation();
/**
* Return a Preview image if available. This will be used as the base layer
* if higher res tiles are not yet available
*/
public BasicTexture getPreview();
/**
* The tile returned by this method can be specified this way: Assuming
* the image size is (width, height), first take the intersection of (0,
* 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
* in extending the region, we found some part of the region is outside
* the image, those pixels are filled with black.
*
* If level > 0, it does the same operation on a down-scaled version of
* the original image (down-scaled by a factor of 2^level), but (x, y)
* still refers to the coordinate on the original image.
*
* The method would be called by the decoder thread.
*/
public Bitmap getTile(int level, int x, int y, Bitmap reuse);
}
public static int suggestedTileSize(Context context) {
return isHighResolution(context) ? 512 : 256;
}
private static boolean isHighResolution(Context context) {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager)
context.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
return metrics.heightPixels > 2048 || metrics.widthPixels > 2048;
}
public TiledImageRenderer(View parent) {
mParent = parent;
mTileDecoder = new TileDecoder();
mTileDecoder.start();
}
public int getViewWidth() {
return mViewWidth;
}
public int getViewHeight() {
return mViewHeight;
}
private void invalidate() {
mParent.postInvalidate();
}
public void setModel(TileSource model, int rotation) {
if (mModel != model) {
mModel = model;
notifyModelInvalidated();
}
if (mRotation != rotation) {
mRotation = rotation;
mLayoutTiles = true;
}
}
private void calculateLevelCount() {
if (mPreview != null) {
mLevelCount = Math.max(0, Utils.ceilLog2(
mImageWidth / (float) mPreview.getWidth()));
} else {
int levels = 1;
int maxDim = Math.max(mImageWidth, mImageHeight);
int t = mTileSize;
while (t < maxDim) {
t <<= 1;
levels++;
}
mLevelCount = levels;
}
}
public void notifyModelInvalidated() {
invalidateTiles();
if (mModel == null) {
mImageWidth = 0;
mImageHeight = 0;
mLevelCount = 0;
mPreview = null;
} else {
mImageWidth = mModel.getImageWidth();
mImageHeight = mModel.getImageHeight();
mPreview = mModel.getPreview();
mTileSize = mModel.getTileSize();
calculateLevelCount();
}
mLayoutTiles = true;
}
public void setViewSize(int width, int height) {
mViewWidth = width;
mViewHeight = height;
}
public void setPosition(int centerX, int centerY, float scale) {
if (mCenterX == centerX && mCenterY == centerY
&& mScale == scale) {
return;
}
mCenterX = centerX;
mCenterY = centerY;
mScale = scale;
mLayoutTiles = true;
}
// Prepare the tiles we want to use for display.
//
// 1. Decide the tile level we want to use for display.
// 2. Decide the tile levels we want to keep as texture (in addition to
// the one we use for display).
// 3. Recycle unused tiles.
// 4. Activate the tiles we want.
private void layoutTiles() {
if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) {
return;
}
mLayoutTiles = false;
// The tile levels we want to keep as texture is in the range
// [fromLevel, endLevel).
int fromLevel;
int endLevel;
// We want to use a texture larger than or equal to the display size.
mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount);
// We want to keep one more tile level as texture in addition to what
// we use for display. So it can be faster when the scale moves to the
// next level. We choose the level closest to the current scale.
if (mLevel != mLevelCount) {
Rect range = mTileRange;
getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation);
mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale);
mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale);
fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
} else {
// Activate the tiles of the smallest two levels.
fromLevel = mLevel - 2;
mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale);
mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale);
}
fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
endLevel = Math.min(fromLevel + 2, mLevelCount);
Rect range[] = mActiveRange;
for (int i = fromLevel; i < endLevel; ++i) {
getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation);
}
// If rotation is transient, don't update the tile.
if (mRotation % 90 != 0) {
return;
}
synchronized (mQueueLock) {
mDecodeQueue.clean();
mUploadQueue.clean();
mBackgroundTileUploaded = false;
// Recycle unused tiles: if the level of the active tile is outside the
// range [fromLevel, endLevel) or not in the visible range.
int n = mActiveTiles.size();
for (int i = 0; i < n; i++) {
Tile tile = mActiveTiles.valueAt(i);
int level = tile.mTileLevel;
if (level < fromLevel || level >= endLevel
|| !range[level - fromLevel].contains(tile.mX, tile.mY)) {
mActiveTiles.removeAt(i);
i--;
n--;
recycleTile(tile);
}
}
}
for (int i = fromLevel; i < endLevel; ++i) {
int size = mTileSize << i;
Rect r = range[i - fromLevel];
for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
for (int x = r.left, right = r.right; x < right; x += size) {
activateTile(x, y, i);
}
}
}
invalidate();
}
private void invalidateTiles() {
synchronized (mQueueLock) {
mDecodeQueue.clean();
mUploadQueue.clean();
// TODO(xx): disable decoder
int n = mActiveTiles.size();
for (int i = 0; i < n; i++) {
Tile tile = mActiveTiles.valueAt(i);
recycleTile(tile);
}
mActiveTiles.clear();
}
}
private void getRange(Rect out, int cX, int cY, int level, int rotation) {
getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
}
// If the bitmap is scaled by the given factor "scale", return the
// rectangle containing visible range. The left-top coordinate returned is
// aligned to the tile boundary.
//
// (cX, cY) is the point on the original bitmap which will be put in the
// center of the ImageViewer.
private void getRange(Rect out,
int cX, int cY, int level, float scale, int rotation) {
double radians = Math.toRadians(-rotation);
double w = mViewWidth;
double h = mViewHeight;
double cos = Math.cos(radians);
double sin = Math.sin(radians);
int width = (int) Math.ceil(Math.max(
Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
int height = (int) Math.ceil(Math.max(
Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
int left = (int) Math.floor(cX - width / (2f * scale));
int top = (int) Math.floor(cY - height / (2f * scale));
int right = (int) Math.ceil(left + width / scale);
int bottom = (int) Math.ceil(top + height / scale);
// align the rectangle to tile boundary
int size = mTileSize << level;
left = Math.max(0, size * (left / size));
top = Math.max(0, size * (top / size));
right = Math.min(mImageWidth, right);
bottom = Math.min(mImageHeight, bottom);
out.set(left, top, right, bottom);
}
public void freeTextures() {
mLayoutTiles = true;
mTileDecoder.finishAndWait();
synchronized (mQueueLock) {
mUploadQueue.clean();
mDecodeQueue.clean();
Tile tile = mRecycledQueue.pop();
while (tile != null) {
tile.recycle();
tile = mRecycledQueue.pop();
}
}
int n = mActiveTiles.size();
for (int i = 0; i < n; i++) {
Tile texture = mActiveTiles.valueAt(i);
texture.recycle();
}
mActiveTiles.clear();
mTileRange.set(0, 0, 0, 0);
while (sTilePool.acquire() != null) {}
}
public boolean draw(GLCanvas canvas) {
layoutTiles();
uploadTiles(canvas);
mUploadQuota = UPLOAD_LIMIT;
mRenderComplete = true;
int level = mLevel;
int rotation = mRotation;
int flags = 0;
if (rotation != 0) {
flags |= GLCanvas.SAVE_FLAG_MATRIX;
}
if (flags != 0) {
canvas.save(flags);
if (rotation != 0) {
int centerX = mViewWidth / 2, centerY = mViewHeight / 2;
canvas.translate(centerX, centerY);
canvas.rotate(rotation, 0, 0, 1);
canvas.translate(-centerX, -centerY);
}
}
try {
if (level != mLevelCount) {
int size = (mTileSize << level);
float length = size * mScale;
Rect r = mTileRange;
for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
float y = mOffsetY + i * length;
for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
float x = mOffsetX + j * length;
drawTile(canvas, tx, ty, level, x, y, length);
}
}
} else if (mPreview != null) {
mPreview.draw(canvas, mOffsetX, mOffsetY,
Math.round(mImageWidth * mScale),
Math.round(mImageHeight * mScale));
}
} finally {
if (flags != 0) {
canvas.restore();
}
}
if (mRenderComplete) {
if (!mBackgroundTileUploaded) {
uploadBackgroundTiles(canvas);
}
} else {
invalidate();
}
return mRenderComplete || mPreview != null;
}
private void uploadBackgroundTiles(GLCanvas canvas) {
mBackgroundTileUploaded = true;
int n = mActiveTiles.size();
for (int i = 0; i < n; i++) {
Tile tile = mActiveTiles.valueAt(i);
if (!tile.isContentValid()) {
queueForDecode(tile);
}
}
}
private void queueForDecode(Tile tile) {
synchronized (mQueueLock) {
if (tile.mTileState == STATE_ACTIVATED) {
tile.mTileState = STATE_IN_QUEUE;
if (mDecodeQueue.push(tile)) {
mQueueLock.notifyAll();
}
}
}
}
private void decodeTile(Tile tile) {
synchronized (mQueueLock) {
if (tile.mTileState != STATE_IN_QUEUE) {
return;
}
tile.mTileState = STATE_DECODING;
}
boolean decodeComplete = tile.decode();
synchronized (mQueueLock) {
if (tile.mTileState == STATE_RECYCLING) {
tile.mTileState = STATE_RECYCLED;
if (tile.mDecodedTile != null) {
sTilePool.release(tile.mDecodedTile);
tile.mDecodedTile = null;
}
mRecycledQueue.push(tile);
return;
}
tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
if (!decodeComplete) {
return;
}
mUploadQueue.push(tile);
}
invalidate();
}
private Tile obtainTile(int x, int y, int level) {
synchronized (mQueueLock) {
Tile tile = mRecycledQueue.pop();
if (tile != null) {
tile.mTileState = STATE_ACTIVATED;
tile.update(x, y, level);
return tile;
}
return new Tile(x, y, level);
}
}
private void recycleTile(Tile tile) {
synchronized (mQueueLock) {
if (tile.mTileState == STATE_DECODING) {
tile.mTileState = STATE_RECYCLING;
return;
}
tile.mTileState = STATE_RECYCLED;
if (tile.mDecodedTile != null) {
sTilePool.release(tile.mDecodedTile);
tile.mDecodedTile = null;
}
mRecycledQueue.push(tile);
}
}
private void activateTile(int x, int y, int level) {
long key = makeTileKey(x, y, level);
Tile tile = mActiveTiles.get(key);
if (tile != null) {
if (tile.mTileState == STATE_IN_QUEUE) {
tile.mTileState = STATE_ACTIVATED;
}
return;
}
tile = obtainTile(x, y, level);
mActiveTiles.put(key, tile);
}
private Tile getTile(int x, int y, int level) {
return mActiveTiles.get(makeTileKey(x, y, level));
}
private static long makeTileKey(int x, int y, int level) {
long result = x;
result = (result << 16) | y;
result = (result << 16) | level;
return result;
}
private void uploadTiles(GLCanvas canvas) {
int quota = UPLOAD_LIMIT;
Tile tile = null;
while (quota > 0) {
synchronized (mQueueLock) {
tile = mUploadQueue.pop();
}
if (tile == null) {
break;
}
if (!tile.isContentValid()) {
if (tile.mTileState == STATE_DECODED) {
tile.updateContent(canvas);
--quota;
} else {
Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
}
}
}
if (tile != null) {
invalidate();
}
}
// Draw the tile to a square at canvas that locates at (x, y) and
// has a side length of length.
private void drawTile(GLCanvas canvas,
int tx, int ty, int level, float x, float y, float length) {
RectF source = mSourceRect;
RectF target = mTargetRect;
target.set(x, y, x + length, y + length);
source.set(0, 0, mTileSize, mTileSize);
Tile tile = getTile(tx, ty, level);
if (tile != null) {
if (!tile.isContentValid()) {
if (tile.mTileState == STATE_DECODED) {
if (mUploadQuota > 0) {
--mUploadQuota;
tile.updateContent(canvas);
} else {
mRenderComplete = false;
}
} else if (tile.mTileState != STATE_DECODE_FAIL){
mRenderComplete = false;
queueForDecode(tile);
}
}
if (drawTile(tile, canvas, source, target)) {
return;
}
}
if (mPreview != null) {
int size = mTileSize << level;
float scaleX = (float) mPreview.getWidth() / mImageWidth;
float scaleY = (float) mPreview.getHeight() / mImageHeight;
source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
(ty + size) * scaleY);
canvas.drawTexture(mPreview, source, target);
}
}
private boolean drawTile(
Tile tile, GLCanvas canvas, RectF source, RectF target) {
while (true) {
if (tile.isContentValid()) {
canvas.drawTexture(tile, source, target);
return true;
}
// Parent can be divided to four quads and tile is one of the four.
Tile parent = tile.getParentTile();
if (parent == null) {
return false;
}
if (tile.mX == parent.mX) {
source.left /= 2f;
source.right /= 2f;
} else {
source.left = (mTileSize + source.left) / 2f;
source.right = (mTileSize + source.right) / 2f;
}
if (tile.mY == parent.mY) {
source.top /= 2f;
source.bottom /= 2f;
} else {
source.top = (mTileSize + source.top) / 2f;
source.bottom = (mTileSize + source.bottom) / 2f;
}
tile = parent;
}
}
private class Tile extends UploadedTexture {
public int mX;
public int mY;
public int mTileLevel;
public Tile mNext;
public Bitmap mDecodedTile;
public volatile int mTileState = STATE_ACTIVATED;
public Tile(int x, int y, int level) {
mX = x;
mY = y;
mTileLevel = level;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
sTilePool.release(bitmap);
}
boolean decode() {
// Get a tile from the original image. The tile is down-scaled
// by (1 << mTilelevel) from a region in the original image.
try {
Bitmap reuse = sTilePool.acquire();
if (reuse != null && reuse.getWidth() != mTileSize) {
reuse = null;
}
mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
} catch (Throwable t) {
Log.w(TAG, "fail to decode tile", t);
}
return mDecodedTile != null;
}
@Override
protected Bitmap onGetBitmap() {
Utils.assertTrue(mTileState == STATE_DECODED);
// We need to override the width and height, so that we won't
// draw beyond the boundaries.
int rightEdge = ((mImageWidth - mX) >> mTileLevel);
int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge));
Bitmap bitmap = mDecodedTile;
mDecodedTile = null;
mTileState = STATE_ACTIVATED;
return bitmap;
}
// We override getTextureWidth() and getTextureHeight() here, so the
// texture can be re-used for different tiles regardless of the actual
// size of the tile (which may be small because it is a tile at the
// boundary).
@Override
public int getTextureWidth() {
return mTileSize;
}
@Override
public int getTextureHeight() {
return mTileSize;
}
public void update(int x, int y, int level) {
mX = x;
mY = y;
mTileLevel = level;
invalidateContent();
}
public Tile getParentTile() {
if (mTileLevel + 1 == mLevelCount) {
return null;
}
int size = mTileSize << (mTileLevel + 1);
int x = size * (mX / size);
int y = size * (mY / size);
return getTile(x, y, mTileLevel + 1);
}
@Override
public String toString() {
return String.format("tile(%s, %s, %s / %s)",
mX / mTileSize, mY / mTileSize, mLevel, mLevelCount);
}
}
private static class TileQueue {
private Tile mHead;
public Tile pop() {
Tile tile = mHead;
if (tile != null) {
mHead = tile.mNext;
}
return tile;
}
public boolean push(Tile tile) {
if (contains(tile)) {
Log.w(TAG, "Attempting to add a tile already in the queue!");
return false;
}
boolean wasEmpty = mHead == null;
tile.mNext = mHead;
mHead = tile;
return wasEmpty;
}
private boolean contains(Tile tile) {
Tile other = mHead;
while (other != null) {
if (other == tile) {
return true;
}
other = other.mNext;
}
return false;
}
public void clean() {
mHead = null;
}
}
private class TileDecoder extends Thread {
public void finishAndWait() {
interrupt();
try {
join();
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!");
}
}
private Tile waitForTile() throws InterruptedException {
synchronized (mQueueLock) {
while (true) {
Tile tile = mDecodeQueue.pop();
if (tile != null) {
return tile;
}
mQueueLock.wait();
}
}
}
@Override
public void run() {
try {
while (!isInterrupted()) {
Tile tile = waitForTile();
decodeTile(tile);
}
} catch (InterruptedException ex) {
// We were finished
}
}
}
}

View File

@@ -0,0 +1,386 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.photos.views;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
import android.view.View;
import android.widget.FrameLayout;
import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.GLES20Canvas;
import com.android.photos.views.TiledImageRenderer.TileSource;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
* or {@link BlockingGLTextureView}.
*/
public class TiledImageView extends FrameLayout {
private static final boolean USE_TEXTURE_VIEW = false;
private static final boolean IS_SUPPORTED =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
private static final boolean USE_CHOREOGRAPHER =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
private BlockingGLTextureView mTextureView;
private GLSurfaceView mGLSurfaceView;
private boolean mInvalPending = false;
private FrameCallback mFrameCallback;
protected static class ImageRendererWrapper {
// Guarded by locks
public float scale;
public int centerX, centerY;
int rotation;
public TileSource source;
Runnable isReadyCallback;
// GL thread only
TiledImageRenderer image;
}
private float[] mValues = new float[9];
// -------------------------
// Guarded by mLock
// -------------------------
protected Object mLock = new Object();
protected ImageRendererWrapper mRenderer;
public static boolean isTilingSupported() {
return IS_SUPPORTED;
}
public TiledImageView(Context context) {
this(context, null);
}
public TiledImageView(Context context, AttributeSet attrs) {
super(context, attrs);
if (!IS_SUPPORTED) {
return;
}
mRenderer = new ImageRendererWrapper();
mRenderer.image = new TiledImageRenderer(this);
View view;
if (USE_TEXTURE_VIEW) {
mTextureView = new BlockingGLTextureView(context);
mTextureView.setRenderer(new TileRenderer());
view = mTextureView;
} else {
mGLSurfaceView = new GLSurfaceView(context);
mGLSurfaceView.setEGLContextClientVersion(2);
mGLSurfaceView.setRenderer(new TileRenderer());
mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
view = mGLSurfaceView;
}
addView(view, new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
//setTileSource(new ColoredTiles());
}
public void destroy() {
if (!IS_SUPPORTED) {
return;
}
if (USE_TEXTURE_VIEW) {
mTextureView.destroy();
} else {
mGLSurfaceView.queueEvent(mFreeTextures);
}
}
private Runnable mFreeTextures = new Runnable() {
@Override
public void run() {
mRenderer.image.freeTextures();
}
};
public void onPause() {
if (!IS_SUPPORTED) {
return;
}
if (!USE_TEXTURE_VIEW) {
mGLSurfaceView.onPause();
}
}
public void onResume() {
if (!IS_SUPPORTED) {
return;
}
if (!USE_TEXTURE_VIEW) {
mGLSurfaceView.onResume();
}
}
public void setTileSource(TileSource source, Runnable isReadyCallback) {
if (!IS_SUPPORTED) {
return;
}
synchronized (mLock) {
mRenderer.source = source;
mRenderer.isReadyCallback = isReadyCallback;
mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
mRenderer.rotation = source != null ? source.getRotation() : 0;
mRenderer.scale = 0;
updateScaleIfNecessaryLocked(mRenderer);
}
invalidate();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!IS_SUPPORTED) {
return;
}
synchronized (mLock) {
updateScaleIfNecessaryLocked(mRenderer);
}
}
private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
if (renderer == null || renderer.source == null
|| renderer.scale > 0 || getWidth() == 0) {
return;
}
renderer.scale = Math.min(
(float) getWidth() / (float) renderer.source.getImageWidth(),
(float) getHeight() / (float) renderer.source.getImageHeight());
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (!IS_SUPPORTED) {
return;
}
if (USE_TEXTURE_VIEW) {
mTextureView.render();
}
super.dispatchDraw(canvas);
}
@SuppressLint("NewApi")
@Override
public void setTranslationX(float translationX) {
if (!IS_SUPPORTED) {
return;
}
super.setTranslationX(translationX);
}
@Override
public void invalidate() {
if (!IS_SUPPORTED) {
return;
}
if (USE_TEXTURE_VIEW) {
super.invalidate();
mTextureView.invalidate();
} else {
if (USE_CHOREOGRAPHER) {
invalOnVsync();
} else {
mGLSurfaceView.requestRender();
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void invalOnVsync() {
if (!mInvalPending) {
mInvalPending = true;
if (mFrameCallback == null) {
mFrameCallback = new FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mInvalPending = false;
mGLSurfaceView.requestRender();
}
};
}
Choreographer.getInstance().postFrameCallback(mFrameCallback);
}
}
private RectF mTempRectF = new RectF();
public void positionFromMatrix(Matrix matrix) {
if (!IS_SUPPORTED) {
return;
}
if (mRenderer.source != null) {
final int rotation = mRenderer.source.getRotation();
final boolean swap = !(rotation % 180 == 0);
final int width = swap ? mRenderer.source.getImageHeight()
: mRenderer.source.getImageWidth();
final int height = swap ? mRenderer.source.getImageWidth()
: mRenderer.source.getImageHeight();
mTempRectF.set(0, 0, width, height);
matrix.mapRect(mTempRectF);
matrix.getValues(mValues);
int cx = width / 2;
int cy = height / 2;
float scale = mValues[Matrix.MSCALE_X];
int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
if (rotation == 90 || rotation == 180) {
cx += (mTempRectF.left / scale) - xoffset;
} else {
cx -= (mTempRectF.left / scale) - xoffset;
}
if (rotation == 180 || rotation == 270) {
cy += (mTempRectF.top / scale) - yoffset;
} else {
cy -= (mTempRectF.top / scale) - yoffset;
}
mRenderer.scale = scale;
mRenderer.centerX = swap ? cy : cx;
mRenderer.centerY = swap ? cx : cy;
invalidate();
}
}
private class TileRenderer implements Renderer {
private GLES20Canvas mCanvas;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mCanvas = new GLES20Canvas();
BasicTexture.invalidateAllTextures();
mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mCanvas.setSize(width, height);
mRenderer.image.setViewSize(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
mCanvas.clearBuffer();
Runnable readyCallback;
synchronized (mLock) {
readyCallback = mRenderer.isReadyCallback;
mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
mRenderer.scale);
}
boolean complete = mRenderer.image.draw(mCanvas);
if (complete && readyCallback != null) {
synchronized (mLock) {
// Make sure we don't trample on a newly set callback/source
// if it changed while we were rendering
if (mRenderer.isReadyCallback == readyCallback) {
mRenderer.isReadyCallback = null;
}
}
if (readyCallback != null) {
post(readyCallback);
}
}
}
}
@SuppressWarnings("unused")
private static class ColoredTiles implements TileSource {
private static final int[] COLORS = new int[] {
Color.RED,
Color.BLUE,
Color.YELLOW,
Color.GREEN,
Color.CYAN,
Color.MAGENTA,
Color.WHITE,
};
private Paint mPaint = new Paint();
private Canvas mCanvas = new Canvas();
@Override
public int getTileSize() {
return 256;
}
@Override
public int getImageWidth() {
return 16384;
}
@Override
public int getImageHeight() {
return 8192;
}
@Override
public int getRotation() {
return 0;
}
@Override
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
int tileSize = getTileSize();
if (bitmap == null) {
bitmap = Bitmap.createBitmap(tileSize, tileSize,
Bitmap.Config.ARGB_8888);
}
mCanvas.setBitmap(bitmap);
mCanvas.drawColor(COLORS[level]);
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(20);
mPaint.setTextAlign(Align.CENTER);
mCanvas.drawText(x + "x" + y, 128, 128, mPaint);
tileSize <<= level;
x /= tileSize;
y /= tileSize;
mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint);
mCanvas.setBitmap(null);
return bitmap;
}
@Override
public BasicTexture getPreview() {
return null;
}
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Copied from Launcher3 */
package com.android.wallpapercropper;
import android.content.Context;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewConfiguration;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import com.android.photos.views.TiledImageRenderer.TileSource;
import com.android.photos.views.TiledImageView;
public class CropView extends TiledImageView implements OnScaleGestureListener {
private ScaleGestureDetector mScaleGestureDetector;
private long mTouchDownTime;
private float mFirstX, mFirstY;
private float mLastX, mLastY;
private float mMinScale;
private boolean mTouchEnabled = true;
private RectF mTempEdges = new RectF();
TouchCallback mTouchCallback;
public interface TouchCallback {
void onTouchDown();
void onTap();
}
public CropView(Context context) {
this(context, null);
}
public CropView(Context context, AttributeSet attrs) {
super(context, attrs);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
}
private void getEdgesHelper(RectF edgesOut) {
final float width = getWidth();
final float height = getHeight();
final float imageWidth = mRenderer.source.getImageWidth();
final float imageHeight = mRenderer.source.getImageHeight();
final float scale = mRenderer.scale;
float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f)
* scale + width / 2f;
float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f)
* scale + height / 2f;
float leftEdge = centerX - imageWidth / 2f * scale;
float rightEdge = centerX + imageWidth / 2f * scale;
float topEdge = centerY - imageHeight / 2f * scale;
float bottomEdge = centerY + imageHeight / 2f * scale;
edgesOut.left = leftEdge;
edgesOut.right = rightEdge;
edgesOut.top = topEdge;
edgesOut.bottom = bottomEdge;
}
public RectF getCrop() {
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
float cropLeft = -edges.left / scale;
float cropTop = -edges.top / scale;
float cropRight = cropLeft + getWidth() / scale;
float cropBottom = cropTop + getHeight() / scale;
return new RectF(cropLeft, cropTop, cropRight, cropBottom);
}
public Point getSourceDimensions() {
return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
}
public void setTileSource(TileSource source, Runnable isReadyCallback) {
super.setTileSource(source, isReadyCallback);
updateMinScale(getWidth(), getHeight(), source, true);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateMinScale(w, h, mRenderer.source, false);
}
public void setScale(float scale) {
synchronized (mLock) {
mRenderer.scale = scale;
}
}
private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
synchronized (mLock) {
if (resetScale) {
mRenderer.scale = 1;
}
if (source != null) {
mMinScale = Math.max(w / (float) source.getImageWidth(),
h / (float) source.getImageHeight());
mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
}
}
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
// Don't need the lock because this will only fire inside of
// onTouchEvent
mRenderer.scale *= detector.getScaleFactor();
mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
invalidate();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
public void moveToUpperLeft() {
if (getWidth() == 0 || getHeight() == 0) {
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
moveToUpperLeft();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
mRenderer.centerX += Math.ceil(edges.left / scale);
mRenderer.centerY += Math.ceil(edges.top / scale);
}
public void setTouchEnabled(boolean enabled) {
mTouchEnabled = enabled;
}
public void setTouchCallback(TouchCallback cb) {
mTouchCallback = cb;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? event.getActionIndex() : -1;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = event.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i)
continue;
sumX += event.getX(i);
sumY += event.getY(i);
}
final int div = pointerUp ? count - 1 : count;
float x = sumX / div;
float y = sumY / div;
if (action == MotionEvent.ACTION_DOWN) {
mFirstX = x;
mFirstY = y;
mTouchDownTime = System.currentTimeMillis();
if (mTouchCallback != null) {
mTouchCallback.onTouchDown();
}
} else if (action == MotionEvent.ACTION_UP) {
ViewConfiguration config = ViewConfiguration.get(getContext());
float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
long now = System.currentTimeMillis();
// only do this if it's a small movement
if (mTouchCallback != null &&
squaredDist < slop &&
now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
mTouchCallback.onTap();
}
}
if (!mTouchEnabled) {
return true;
}
synchronized (mLock) {
mScaleGestureDetector.onTouchEvent(event);
switch (action) {
case MotionEvent.ACTION_MOVE:
mRenderer.centerX += (mLastX - x) / mRenderer.scale;
mRenderer.centerY += (mLastY - y) / mRenderer.scale;
invalidate();
break;
}
if (mRenderer.source != null) {
// Adjust position so that the wallpaper covers the entire area
// of the screen
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
if (edges.left > 0) {
mRenderer.centerX += Math.ceil(edges.left / scale);
}
if (edges.right < getWidth()) {
mRenderer.centerX += (edges.right - getWidth()) / scale;
}
if (edges.top > 0) {
mRenderer.centerY += Math.ceil(edges.top / scale);
}
if (edges.bottom < getHeight()) {
mRenderer.centerY += (edges.bottom - getHeight()) / scale;
}
}
}
mLastX = x;
mLastY = y;
return true;
}
}

View File

@@ -0,0 +1,646 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Copied from Launcher3 */
package com.android.wallpapercropper;
import android.app.ActionBar;
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import com.android.gallery3d.common.Utils;
import com.android.photos.BitmapRegionTileSource;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class WallpaperCropActivity extends Activity {
private static final String LOGTAG = "Launcher3.CropActivity";
protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
private static final int DEFAULT_COMPRESS_QUALITY = 90;
/**
* The maximum bitmap size we allow to be returned through the intent.
* Intents have a maximum of 1MB in total size. However, the Bitmap seems to
* have some overhead to hit so that we go way below the limit here to make
* sure the intent stays below 1MB.We should consider just returning a byte
* array instead of a Bitmap instance to avoid overhead.
*/
public static final int MAX_BMAP_IN_INTENT = 750000;
private static final float WALLPAPER_SCREENS_SPAN = 2f;
protected CropView mCropView;
protected Uri mUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
init();
}
protected void init() {
setContentView(R.layout.wallpaper_cropper);
mCropView = (CropView) findViewById(R.id.cropView);
Intent cropIntent = this.getIntent();
final Uri imageUri = cropIntent.getData();
mCropView.setTileSource(new BitmapRegionTileSource(this, imageUri, 1024, 0), null);
mCropView.setTouchEnabled(true);
// Action bar
// Show the custom action bar view
final ActionBar actionBar = getActionBar();
actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
actionBar.getCustomView().setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean finishActivityWhenDone = true;
cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
}
});
}
public static String getSharedPreferencesKey() {
return WallpaperCropActivity.class.getName();
}
// As a ratio of screen height, the total distance we want the parallax effect to span
// horizontally
private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
float aspectRatio = width / (float) height;
// At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
// At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
// We will use these two data points to extrapolate how much the wallpaper parallax effect
// to span (ie travel) at any aspect ratio:
final float ASPECT_RATIO_LANDSCAPE = 16/10f;
final float ASPECT_RATIO_PORTRAIT = 10/16f;
final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
// To find out the desired width at different aspect ratios, we use the following two
// formulas, where the coefficient on x is the aspect ratio (width/height):
// (16/10)x + y = 1.5
// (10/16)x + y = 1.2
// We solve for x and y and end up with a final formula:
final float x =
(WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
(ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
return x * aspectRatio + y;
}
static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
Point minDims = new Point();
Point maxDims = new Point();
windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
int maxDim = Math.max(maxDims.x, maxDims.y);
final int minDim = Math.min(minDims.x, minDims.y);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
Point realSize = new Point();
windowManager.getDefaultDisplay().getRealSize(realSize);
maxDim = Math.max(realSize.x, realSize.y);
}
// We need to ensure that there is enough extra space in the wallpaper
// for the intended
// parallax effects
final int defaultWidth, defaultHeight;
if (isScreenLarge(res)) {
defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
defaultHeight = maxDim;
} else {
defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
defaultHeight = maxDim;
}
return new Point(defaultWidth, defaultHeight);
}
protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
BitmapCropTask cropTask = new BitmapCropTask(this,
filePath, null, 0, 0, true, false, null);
final Point bounds = cropTask.getImageBounds();
Runnable onEndCrop = new Runnable() {
public void run() {
updateWallpaperDimensions(bounds.x, bounds.y);
if (finishActivityWhenDone) {
setResult(Activity.RESULT_OK);
finish();
}
}
};
cropTask.setOnEndRunnable(onEndCrop);
cropTask.setNoCrop(true);
cropTask.execute();
}
protected void cropImageAndSetWallpaper(
Resources res, int resId, final boolean finishActivityWhenDone) {
// crop this image and scale it down to the default wallpaper size for
// this device
Point inSize = mCropView.getSourceDimensions();
Point outSize = getDefaultWallpaperSize(getResources(),
getWindowManager());
RectF crop = getMaxCropRect(
inSize.x, inSize.y, outSize.x, outSize.y, false);
Runnable onEndCrop = new Runnable() {
public void run() {
// Passing 0, 0 will cause launcher to revert to using the
// default wallpaper size
updateWallpaperDimensions(0, 0);
if (finishActivityWhenDone) {
setResult(Activity.RESULT_OK);
finish();
}
}
};
BitmapCropTask cropTask = new BitmapCropTask(res, resId,
crop, outSize.x, outSize.y,
true, false, onEndCrop);
cropTask.execute();
}
private static boolean isScreenLarge(Resources res) {
Configuration config = res.getConfiguration();
return config.smallestScreenWidthDp >= 720;
}
protected void cropImageAndSetWallpaper(Uri uri,
OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
// Get the crop
Point inSize = mCropView.getSourceDimensions();
Point minDims = new Point();
Point maxDims = new Point();
Display d = getWindowManager().getDefaultDisplay();
d.getCurrentSizeRange(minDims, maxDims);
Point displaySize = new Point();
d.getSize(displaySize);
int maxDim = Math.max(maxDims.x, maxDims.y);
final int minDim = Math.min(minDims.x, minDims.y);
int defaultWidth;
if (isScreenLarge(getResources())) {
defaultWidth = (int) (maxDim *
wallpaperTravelToScreenWidthRatio(maxDim, minDim));
} else {
defaultWidth = Math.max((int)
(minDim * WALLPAPER_SCREENS_SPAN), maxDim);
}
boolean isPortrait = displaySize.x < displaySize.y;
int portraitHeight;
if (isPortrait) {
portraitHeight = mCropView.getHeight();
} else {
// TODO: how to actually get the proper portrait height?
// This is not quite right:
portraitHeight = Math.max(maxDims.x, maxDims.y);
}
if (android.os.Build.VERSION.SDK_INT >=
android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
Point realSize = new Point();
d.getRealSize(realSize);
portraitHeight = Math.max(realSize.x, realSize.y);
}
// Get the crop
RectF cropRect = mCropView.getCrop();
float cropScale = mCropView.getWidth() / (float) cropRect.width();
// ADJUST CROP WIDTH
// Extend the crop all the way to the right, for parallax
float extraSpaceToRight = inSize.x - cropRect.right;
// Cap the amount of extra width
float maxExtraSpace = defaultWidth / cropScale - cropRect.width();
extraSpaceToRight = Math.min(extraSpaceToRight, maxExtraSpace);
cropRect.right += extraSpaceToRight;
// ADJUST CROP HEIGHT
if (isPortrait) {
cropRect.bottom = cropRect.top + portraitHeight / cropScale;
} else { // LANDSCAPE
float extraPortraitHeight =
portraitHeight / cropScale - cropRect.height();
float expandHeight =
Math.min(Math.min(inSize.y - cropRect.bottom, cropRect.top),
extraPortraitHeight / 2);
cropRect.top -= expandHeight;
cropRect.bottom += expandHeight;
}
final int outWidth = (int) Math.round(cropRect.width() * cropScale);
final int outHeight = (int) Math.round(cropRect.height() * cropScale);
Runnable onEndCrop = new Runnable() {
public void run() {
updateWallpaperDimensions(outWidth, outHeight);
if (finishActivityWhenDone) {
setResult(Activity.RESULT_OK);
finish();
}
}
};
BitmapCropTask cropTask = new BitmapCropTask(uri,
cropRect, outWidth, outHeight, true, false, onEndCrop);
if (onBitmapCroppedHandler != null) {
cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
}
cropTask.execute();
}
public interface OnBitmapCroppedHandler {
public void onBitmapCropped(byte[] imageBytes);
}
protected class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
Uri mInUri = null;
Context mContext;
String mInFilePath;
byte[] mInImageBytes;
int mInResId = 0;
InputStream mInStream;
RectF mCropBounds = null;
int mOutWidth, mOutHeight;
int mRotation = 0; // for now
protected final WallpaperManager mWPManager;
String mOutputFormat = "jpg"; // for now
boolean mSetWallpaper;
boolean mSaveCroppedBitmap;
Bitmap mCroppedBitmap;
Runnable mOnEndRunnable;
Resources mResources;
OnBitmapCroppedHandler mOnBitmapCroppedHandler;
boolean mNoCrop;
public BitmapCropTask(Context c, String filePath,
RectF cropBounds, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mContext = c;
mInFilePath = filePath;
mWPManager = WallpaperManager.getInstance(getApplicationContext());
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(byte[] imageBytes,
RectF cropBounds, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mInImageBytes = imageBytes;
mWPManager = WallpaperManager.getInstance(getApplicationContext());
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(Uri inUri,
RectF cropBounds, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mInUri = inUri;
mWPManager = WallpaperManager.getInstance(getApplicationContext());
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
public BitmapCropTask(Resources res, int inResId,
RectF cropBounds, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mInResId = inResId;
mResources = res;
mWPManager = WallpaperManager.getInstance(getApplicationContext());
init(cropBounds, outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
}
private void init(RectF cropBounds, int outWidth, int outHeight,
boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
mCropBounds = cropBounds;
mOutWidth = outWidth;
mOutHeight = outHeight;
mSetWallpaper = setWallpaper;
mSaveCroppedBitmap = saveCroppedBitmap;
mOnEndRunnable = onEndRunnable;
}
public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
mOnBitmapCroppedHandler = handler;
}
public void setNoCrop(boolean value) {
mNoCrop = value;
}
public void setOnEndRunnable(Runnable onEndRunnable) {
mOnEndRunnable = onEndRunnable;
}
// Helper to setup input stream
private void regenerateInputStream() {
if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
"image byte array given");
} else {
Utils.closeSilently(mInStream);
try {
if (mInUri != null) {
mInStream = new BufferedInputStream(
getContentResolver().openInputStream(mInUri));
} else if (mInFilePath != null) {
mInStream = mContext.openFileInput(mInFilePath);
} else if (mInImageBytes != null) {
mInStream = new BufferedInputStream(
new ByteArrayInputStream(mInImageBytes));
} else {
mInStream = new BufferedInputStream(
mResources.openRawResource(mInResId));
}
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
}
}
}
public Point getImageBounds() {
regenerateInputStream();
if (mInStream != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(mInStream, null, options);
if (options.outWidth != 0 && options.outHeight != 0) {
return new Point(options.outWidth, options.outHeight);
}
}
return null;
}
public void setCropBounds(RectF cropBounds) {
mCropBounds = cropBounds;
}
public Bitmap getCroppedBitmap() {
return mCroppedBitmap;
}
public boolean cropBitmap() {
boolean failure = false;
regenerateInputStream();
if (mNoCrop && mInStream != null) {
try {
mWPManager.setStream(mInStream);
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
if (mOnEndRunnable != null) {
mOnEndRunnable.run();
}
return !failure;
}
if (mInStream != null) {
// Find crop bounds (scaled to original image size)
Rect roundedTrueCrop = new Rect();
mCropBounds.roundOut(roundedTrueCrop);
if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
Log.w(LOGTAG, "crop has bad values for full size image");
failure = true;
return false;
}
// See how much we're reducing the size of the image
int scaleDownSampleSize = Math.min(roundedTrueCrop.width() / mOutWidth,
roundedTrueCrop.height() / mOutHeight);
// Attempt to open a region decoder
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInStream, true);
} catch (IOException e) {
Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
}
Bitmap crop = null;
if (decoder != null) {
// Do region decoding to get crop bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
if (scaleDownSampleSize > 1) {
options.inSampleSize = scaleDownSampleSize;
}
crop = decoder.decodeRegion(roundedTrueCrop, options);
decoder.recycle();
}
if (crop == null) {
// BitmapRegionDecoder has failed, try to crop in-memory
regenerateInputStream();
Bitmap fullSize = null;
if (mInStream != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
if (scaleDownSampleSize > 1) {
options.inSampleSize = scaleDownSampleSize;
}
fullSize = BitmapFactory.decodeStream(mInStream, null, options);
}
if (fullSize != null) {
crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
roundedTrueCrop.top, roundedTrueCrop.width(),
roundedTrueCrop.height());
}
}
if (crop == null) {
Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
failure = true;
return false;
}
if (mOutWidth > 0 && mOutHeight > 0) {
Matrix m = new Matrix();
RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
if (mRotation > 0) {
m.setRotate(mRotation);
m.mapRect(cropRect);
}
RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
m.preRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
(int) returnRect.height(), Bitmap.Config.ARGB_8888);
if (tmp != null) {
Canvas c = new Canvas(tmp);
c.drawBitmap(crop, m, new Paint());
crop = tmp;
}
} else if (mRotation > 0) {
Matrix m = new Matrix();
m.setRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
crop.getHeight(), m, true);
if (tmp != null) {
crop = tmp;
}
}
if (mSaveCroppedBitmap) {
mCroppedBitmap = crop;
}
// Get output compression format
CompressFormat cf =
convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
// Compress to byte array
ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
// If we need to set to the wallpaper, set it
if (mSetWallpaper && mWPManager != null) {
if (mWPManager == null) {
Log.w(LOGTAG, "no wallpaper manager");
failure = true;
} else {
try {
byte[] outByteArray = tmpOut.toByteArray();
mWPManager.setStream(new ByteArrayInputStream(outByteArray));
if (mOnBitmapCroppedHandler != null) {
mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
}
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
}
}
if (mOnEndRunnable != null) {
mOnEndRunnable.run();
}
} else {
Log.w(LOGTAG, "cannot compress bitmap");
failure = true;
}
}
return !failure; // True if any of the operations failed
}
@Override
protected Boolean doInBackground(Void... params) {
return cropBitmap();
}
@Override
protected void onPostExecute(Boolean result) {
setResult(Activity.RESULT_OK);
finish();
}
}
protected void updateWallpaperDimensions(int width, int height) {
String spKey = getSharedPreferencesKey();
SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
if (width != 0 && height != 0) {
editor.putInt(WALLPAPER_WIDTH_KEY, width);
editor.putInt(WALLPAPER_HEIGHT_KEY, height);
} else {
editor.remove(WALLPAPER_WIDTH_KEY);
editor.remove(WALLPAPER_HEIGHT_KEY);
}
editor.commit();
suggestWallpaperDimension(getResources(),
sp, getWindowManager(), WallpaperManager.getInstance(this));
}
static public void suggestWallpaperDimension(Resources res,
final SharedPreferences sharedPrefs,
WindowManager windowManager,
final WallpaperManager wallpaperManager) {
final Point defaultWallpaperSize =
WallpaperCropActivity.getDefaultWallpaperSize(res, windowManager);
new Thread("suggestWallpaperDimension") {
public void run() {
// If we have saved a wallpaper width/height, use that instead
int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWallpaperSize.x);
int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultWallpaperSize.y);
wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
}
}.start();
}
protected static RectF getMaxCropRect(
int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
RectF cropRect = new RectF();
// Get a crop rect that will fit this
if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
cropRect.top = 0;
cropRect.bottom = inHeight;
cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
cropRect.right = inWidth - cropRect.left;
if (leftAligned) {
cropRect.right -= cropRect.left;
cropRect.left = 0;
}
} else {
cropRect.left = 0;
cropRect.right = inWidth;
cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
cropRect.bottom = inHeight - cropRect.top;
}
return cropRect;
}
protected static CompressFormat convertExtensionToCompressFormat(String extension) {
return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
}
protected static String getFileExtension(String requestFormat) {
String outputFormat = (requestFormat == null)
? "jpg"
: requestFormat;
outputFormat = outputFormat.toLowerCase();
return (outputFormat.equals("png") || outputFormat.equals("gif"))
? "png" // We don't support gif compression.
: "jpg";
}
}