Merge "Fix suggestion window issues." into nyc-dev
diff --git a/api/current.txt b/api/current.txt
index 1421784..f19ac5c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20207,6 +20207,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -20228,6 +20230,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20243,9 +20249,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/system-current.txt b/api/system-current.txt
index 71b0aeb..b534b1e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -21715,6 +21715,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -21736,6 +21738,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -21751,9 +21757,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/test-current.txt b/api/test-current.txt
index 83e4642..0b1c208 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -20216,6 +20216,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -20237,6 +20239,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20252,9 +20258,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 332c739..4792dc9 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -815,6 +815,9 @@
private int mDefaultKeyMode = DEFAULT_KEYS_DISABLE;
private SpannableStringBuilder mDefaultKeySsb = null;
+ private ActivityManager.TaskDescription mTaskDescription =
+ new ActivityManager.TaskDescription();
+
protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused};
@SuppressWarnings("unused")
@@ -1116,6 +1119,34 @@
}
/**
+ * Attempts to extract the color from a given drawable.
+ *
+ * @return the extracted color or 0 if no color could be extracted.
+ */
+ private int tryExtractColorFromDrawable(Drawable drawable) {
+ if (drawable instanceof ColorDrawable) {
+ return ((ColorDrawable) drawable).getColor();
+ } else if (drawable instanceof InsetDrawable) {
+ return tryExtractColorFromDrawable(((InsetDrawable) drawable).getDrawable());
+ } else if (drawable instanceof ShapeDrawable) {
+ Paint p = ((ShapeDrawable) drawable).getPaint();
+ if (p != null) {
+ return p.getColor();
+ }
+ } else if (drawable instanceof LayerDrawable) {
+ LayerDrawable ld = (LayerDrawable) drawable;
+ int numLayers = ld.getNumberOfLayers();
+ for (int i = 0; i < numLayers; i++) {
+ int color = tryExtractColorFromDrawable(ld.getDrawable(i));
+ if (color != 0) {
+ return color;
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
* Called when activity start-up is complete (after {@link #onStart}
* and {@link #onRestoreInstanceState} have been called). Applications will
* generally not implement this method; it is intended for system
@@ -1136,6 +1167,36 @@
mTitleReady = true;
onTitleChanged(getTitle(), getTitleColor());
}
+
+ Resources.Theme theme = getTheme();
+ if (theme != null) {
+ // Get the primary color and update the TaskDescription for this activity
+ TypedArray a = theme.obtainStyledAttributes(
+ com.android.internal.R.styleable.ActivityTaskDescription);
+ if (mTaskDescription.getPrimaryColor() == 0) {
+ int colorPrimary = a.getColor(
+ com.android.internal.R.styleable.ActivityTaskDescription_colorPrimary, 0);
+ if (colorPrimary != 0 && Color.alpha(colorPrimary) == 0xFF) {
+ mTaskDescription.setPrimaryColor(colorPrimary);
+ }
+ }
+ if (mTaskDescription.getBackgroundColor() == 0) {
+ int windowBgResourceId = a.getResourceId(
+ com.android.internal.R.styleable.ActivityTaskDescription_windowBackground,
+ 0);
+ int windowBgFallbackResourceId = a.getResourceId(
+ com.android.internal.R.styleable.ActivityTaskDescription_windowBackgroundFallback,
+ 0);
+ int colorBg = tryExtractColorFromDrawable(DecorView.getResizingBackgroundDrawable(
+ this, windowBgResourceId, windowBgFallbackResourceId));
+ if (colorBg != 0 && Color.alpha(colorBg) == 0xFF) {
+ mTaskDescription.setBackgroundColor(colorBg);
+ }
+ }
+ a.recycle();
+ setTaskDescription(mTaskDescription);
+ }
+
mCalled = true;
}
@@ -3975,57 +4036,6 @@
}
theme.applyStyle(resid, false);
}
-
- // Get the primary color and update the TaskDescription for this activity
- if (theme != null) {
- TypedArray a = theme.obtainStyledAttributes(com.android.internal.R.styleable.Theme);
- int windowBgResourceId = a.getResourceId(
- com.android.internal.R.styleable.Window_windowBackground, 0);
- int windowBgFallbackResourceId = a.getResourceId(
- com.android.internal.R.styleable.Window_windowBackgroundFallback, 0);
- int colorPrimary = a.getColor(com.android.internal.R.styleable.Theme_colorPrimary, 0);
- int colorBg = tryExtractColorFromDrawable(DecorView.getResizingBackgroundDrawable(this,
- windowBgResourceId, windowBgFallbackResourceId));
- a.recycle();
- if (colorPrimary != 0) {
- ActivityManager.TaskDescription td = new ActivityManager.TaskDescription();
- if (Color.alpha(colorPrimary) == 0xFF) {
- td.setPrimaryColor(colorPrimary);
- }
- if (Color.alpha(colorBg) == 0xFF) {
- td.setBackgroundColor(colorBg);
- }
- setTaskDescription(td);
- }
- }
- }
-
- /**
- * Attempts to extract the color from a given drawable.
- *
- * @return the extracted color or 0 if no color could be extracted.
- */
- private int tryExtractColorFromDrawable(Drawable drawable) {
- if (drawable instanceof ColorDrawable) {
- return ((ColorDrawable) drawable).getColor();
- } else if (drawable instanceof InsetDrawable) {
- return tryExtractColorFromDrawable(((InsetDrawable) drawable).getDrawable());
- } else if (drawable instanceof ShapeDrawable) {
- Paint p = ((ShapeDrawable) drawable).getPaint();
- if (p != null) {
- return p.getColor();
- }
- } else if (drawable instanceof LayerDrawable) {
- LayerDrawable ld = (LayerDrawable) drawable;
- int numLayers = ld.getNumberOfLayers();
- for (int i = 0; i < numLayers; i++) {
- int color = tryExtractColorFromDrawable(ld.getDrawable(i));
- if (color != 0) {
- return color;
- }
- }
- }
- return 0;
}
/**
@@ -5651,18 +5661,18 @@
* @param taskDescription The TaskDescription properties that describe the task with this activity
*/
public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
- ActivityManager.TaskDescription td;
- // Scale the icon down to something reasonable if it is provided
- if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
- final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
- final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size, true);
- td = new ActivityManager.TaskDescription(taskDescription);
- td.setIcon(icon);
- } else {
- td = taskDescription;
+ if (mTaskDescription != taskDescription) {
+ mTaskDescription.copyFrom(taskDescription);
+ // Scale the icon down to something reasonable if it is provided
+ if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
+ final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
+ final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
+ true);
+ mTaskDescription.setIcon(icon);
+ }
}
try {
- ActivityManagerNative.getDefault().setTaskDescription(mToken, td);
+ ActivityManagerNative.getDefault().setTaskDescription(mToken, mTaskDescription);
} catch (RemoteException e) {
}
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 7771139..b1927d0 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -946,11 +946,19 @@
* Creates a copy of another TaskDescription.
*/
public TaskDescription(TaskDescription td) {
- mLabel = td.mLabel;
- mIcon = td.mIcon;
- mIconFilename = td.mIconFilename;
- mColorPrimary = td.mColorPrimary;
- mColorBackground = td.mColorBackground;
+ copyFrom(td);
+ }
+
+ /**
+ * Copies this the values from another TaskDescription.
+ * @hide
+ */
+ public void copyFrom(TaskDescription other) {
+ mLabel = other.mLabel;
+ mIcon = other.mIcon;
+ mIconFilename = other.mIconFilename;
+ mColorPrimary = other.mColorPrimary;
+ mColorBackground = other.mColorBackground;
}
private TaskDescription(Parcel source) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 26f3b39..e7be7af 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -18916,7 +18916,8 @@
transformFromViewToWindowSpace(outLocation);
}
- void transformFromViewToWindowSpace(@Size(2) int[] inOutLocation) {
+ /** @hide */
+ public void transformFromViewToWindowSpace(@Size(2) int[] inOutLocation) {
if (inOutLocation == null || inOutLocation.length < 2) {
throw new IllegalArgumentException("inOutLocation must be an array of two integers");
}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index b2ef687..881e5cd 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -4131,13 +4131,15 @@
}
if (isVisible()) {
- final int positionX = parentPositionX + mPositionX;
- final int positionY = parentPositionY + mPositionY;
+ // Transform to the window coordinates to follow the view tranformation.
+ final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
+ mTextView.transformFromViewToWindowSpace(pts);
+ pts[0] -= mHotspotX + getHorizontalOffset();
+
if (isShowing()) {
- mContainer.update(positionX, positionY, -1, -1);
+ mContainer.update(pts[0], pts[1], -1, -1);
} else {
- mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
- positionX, positionY);
+ mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
}
} else {
if (isShowing()) {
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3a45da9..7c02128 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8182,4 +8182,18 @@
<!-- The current color for the offset inside the gradient. -->
<attr name="color" />
</declare-styleable>
+
+ <!-- @hide Attributes which will be read by the Activity to intialize the
+ base activity TaskDescription. -->
+ <declare-styleable name="ActivityTaskDescription">
+ <!-- @hide From Theme.colorPrimary, used for the TaskDescription primary
+ color. -->
+ <attr name="colorPrimary" />
+ <!-- @hide From Theme.windowBackground, used for calculating the
+ TaskDescription background color. -->
+ <attr name="windowBackground" />
+ <!-- @hide From Theme.windowBackgroundFallback, used for calculating the
+ TaskDescription background color. -->
+ <attr name="windowBackgroundFallback" />
+ </declare-styleable>
</resources>
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 7fb67ee..1a387be 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -16,22 +16,51 @@
package android.media;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.regex.Pattern;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
/**
* This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
* <p>
* Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF and RAF.
+ * <p>
+ * Attribute mutation is supported for JPEG image files.
*/
public class ExifInterface {
+ private static final String TAG = "ExifInterface";
+ private static final boolean DEBUG = false;
+
// The Exif tag names
/** Type is int. */
public static final String TAG_ORIENTATION = "Orientation";
@@ -97,6 +126,20 @@
public static final String TAG_FOCAL_LENGTH = "FocalLength";
/** Type is String. Name of GPS processing method used for location finding. */
public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
+ /** Type is double. */
+ public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ /** Type is double. */
+ public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
+ /** Type is double. */
+ public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ /** Type is int. */
+ public static final String TAG_LIGHT_SOURCE = "LightSource";
+ /** Type is int. */
+ public static final String TAG_METERING_MODE = "MeteringMode";
+ /** Type is int. */
+ public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
+ /** Type is int. */
+ public static final String TAG_EXPOSURE_MODE = "ExposureMode";
// Private tags used for thumbnail information.
private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
@@ -119,30 +162,321 @@
// Constants used for white balance
public static final int WHITEBALANCE_AUTO = 0;
public static final int WHITEBALANCE_MANUAL = 1;
+
private static SimpleDateFormat sFormatter;
+ // See Exchangeable image file format for digital still cameras: Exif version 2.2.
+ // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
+ // They are called "Image File Directory". They have multiple data formats to cover various
+ // image metadata from GPS longitude to camera model name.
+
+ // Types of Exif byte alignments (see JEITA CP-3451 page 10)
+ private static final short BYTE_ALIGN_II = 0x4949; // II: Intel order
+ private static final short BYTE_ALIGN_MM = 0x4d4d; // MM: Motorola order
+
+ // Formats for the value in IFD entry (See TIFF 6.0 spec Types page 15).
+ private static final int IFD_FORMAT_BYTE = 1;
+ private static final int IFD_FORMAT_STRING = 2;
+ private static final int IFD_FORMAT_USHORT = 3;
+ private static final int IFD_FORMAT_ULONG = 4;
+ private static final int IFD_FORMAT_URATIONAL = 5;
+ private static final int IFD_FORMAT_SBYTE = 6;
+ private static final int IFD_FORMAT_UNDEFINED = 7;
+ private static final int IFD_FORMAT_SSHORT = 8;
+ private static final int IFD_FORMAT_SLONG = 9;
+ private static final int IFD_FORMAT_SRATIONAL = 10;
+ private static final int IFD_FORMAT_SINGLE = 11;
+ private static final int IFD_FORMAT_DOUBLE = 12;
+ // Sizes of the components of each IFD value format
+ private static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+ 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8
+ };
+ private static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+ 0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+ };
+
+ // A class for indicating EXIF tag.
+ private static class ExifTag {
+ public final int number;
+ public final String name;
+
+ private ExifTag(String name, int number) {
+ this.name = name;
+ this.number = number;
+ }
+ }
+
+ // Primary image IFD TIFF tags (See JEITA CP-3451 Table 14. page 54).
+ private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
+ new ExifTag("ImageWidth", 256),
+ new ExifTag("ImageLength", 257),
+ new ExifTag("BitsPerSample", 258),
+ new ExifTag("Compression", 259),
+ new ExifTag("PhotometricInterpretation", 262),
+ new ExifTag("ImageDescription", 270),
+ new ExifTag("Make", 271),
+ new ExifTag("Model", 272),
+ new ExifTag("StripOffsets", 273),
+ new ExifTag("Orientation", 274),
+ new ExifTag("SamplesPerPixel", 277),
+ new ExifTag("RowsPerStrip", 278),
+ new ExifTag("StripByteCounts", 279),
+ new ExifTag("XResolution", 282),
+ new ExifTag("YResolution", 283),
+ new ExifTag("PlanarConfiguration", 284),
+ new ExifTag("ResolutionUnit", 296),
+ new ExifTag("TransferFunction", 301),
+ new ExifTag("Software", 305),
+ new ExifTag("DateTime", 306),
+ new ExifTag("Artist", 315),
+ new ExifTag("WhitePoint", 318),
+ new ExifTag("PrimaryChromaticities", 319),
+ new ExifTag("JPEGInterchangeFormat", 513),
+ new ExifTag("JPEGInterchangeFormatLength", 514),
+ new ExifTag("YCbCrCoefficients", 529),
+ new ExifTag("YCbCrSubSampling", 530),
+ new ExifTag("YCbCrPositioning", 531),
+ new ExifTag("ReferenceBlackWhite", 532),
+ new ExifTag("Copyright", 33432),
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoIFDPointer", 34853),
+ };
+ // Primary image IFD Exif Private tags (See JEITA CP-3451 Table 15. page 55).
+ private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
+ new ExifTag("ExposureTime", 33434),
+ new ExifTag("FNumber", 33437),
+ new ExifTag("ExposureProgram", 34850),
+ new ExifTag("SpectralSensitivity", 34852),
+ new ExifTag("ISOSpeedRatings", 34855),
+ new ExifTag("OECF", 34856),
+ new ExifTag("ExifVersion", 36864),
+ new ExifTag("DateTimeOriginal", 36867),
+ new ExifTag("DateTimeDigitized", 36868),
+ new ExifTag("ComponentsConfiguration", 37121),
+ new ExifTag("CompressedBitsPerPixel", 37122),
+ new ExifTag("ShutterSpeedValue", 37377),
+ new ExifTag("ApertureValue", 37378),
+ new ExifTag("BrightnessValue", 37379),
+ new ExifTag("ExposureBiasValue", 37380),
+ new ExifTag("MaxApertureValue", 37381),
+ new ExifTag("SubjectDistance", 37382),
+ new ExifTag("MeteringMode", 37383),
+ new ExifTag("LightSource", 37384),
+ new ExifTag("Flash", 37385),
+ new ExifTag("FocalLength", 37386),
+ new ExifTag("SubjectArea", 37396),
+ new ExifTag("MakerNote", 37500),
+ new ExifTag("UserComment", 37510),
+ new ExifTag("SubSecTime", 37520),
+ new ExifTag("SubSecTimeOriginal", 37521),
+ new ExifTag("SubSecTimeDigitized", 37522),
+ new ExifTag("FlashpixVersion", 40960),
+ new ExifTag("ColorSpace", 40961),
+ new ExifTag("PixelXDimension", 40962),
+ new ExifTag("PixelYDimension", 40963),
+ new ExifTag("RelatedSoundFile", 40964),
+ new ExifTag("InteroperabilityIFDPointer", 40965),
+ new ExifTag("FlashEnergy", 41483),
+ new ExifTag("SpatialFrequencyResponse", 41484),
+ new ExifTag("FocalPlaneXResolution", 41486),
+ new ExifTag("FocalPlaneYResolution", 41487),
+ new ExifTag("FocalPlaneResolutionUnit", 41488),
+ new ExifTag("SubjectLocation", 41492),
+ new ExifTag("ExposureIndex", 41493),
+ new ExifTag("SensingMethod", 41495),
+ new ExifTag("FileSource", 41728),
+ new ExifTag("SceneType", 41729),
+ new ExifTag("CFAPattern", 41730),
+ new ExifTag("CustomRendered", 41985),
+ new ExifTag("ExposureMode", 41986),
+ new ExifTag("WhiteBalance", 41987),
+ new ExifTag("DigitalZoomRatio", 41988),
+ new ExifTag("FocalLengthIn35mmFilm", 41989),
+ new ExifTag("SceneCaptureType", 41990),
+ new ExifTag("GainControl", 41991),
+ new ExifTag("Contrast", 41992),
+ new ExifTag("Saturation", 41993),
+ new ExifTag("Sharpness", 41994),
+ new ExifTag("DeviceSettingDescription", 41995),
+ new ExifTag("SubjectDistanceRange", 41996),
+ new ExifTag("ImageUniqueID", 42016),
+ };
+ // Primary image IFD GPS Info tags (See JEITA CP-3451 Table 16. page 56).
+ private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
+ new ExifTag("GPSVersionID", 0),
+ new ExifTag("GPSLatitudeRef", 1),
+ new ExifTag("GPSLatitude", 2),
+ new ExifTag("GPSLongitudeRef", 3),
+ new ExifTag("GPSLongitude", 4),
+ new ExifTag("GPSAltitudeRef", 5),
+ new ExifTag("GPSAltitude", 6),
+ new ExifTag("GPSTimeStamp", 7),
+ new ExifTag("GPSSatellites", 8),
+ new ExifTag("GPSStatus", 9),
+ new ExifTag("GPSMeasureMode", 10),
+ new ExifTag("GPSDOP", 11),
+ new ExifTag("GPSSpeedRef", 12),
+ new ExifTag("GPSSpeed", 13),
+ new ExifTag("GPSTrackRef", 14),
+ new ExifTag("GPSTrack", 15),
+ new ExifTag("GPSImgDirectionRef", 16),
+ new ExifTag("GPSImgDirection", 17),
+ new ExifTag("GPSMapDatum", 18),
+ new ExifTag("GPSDestLatitudeRef", 19),
+ new ExifTag("GPSDestLatitude", 20),
+ new ExifTag("GPSDestLongitudeRef", 21),
+ new ExifTag("GPSDestLongitude", 22),
+ new ExifTag("GPSDestBearingRef", 23),
+ new ExifTag("GPSDestBearing", 24),
+ new ExifTag("GPSDestDistanceRef", 25),
+ new ExifTag("GPSDestDistance", 26),
+ new ExifTag("GPSProcessingMethod", 27),
+ new ExifTag("GPSAreaInformation", 28),
+ new ExifTag("GPSDateStamp", 29),
+ new ExifTag("GPSDifferential", 30),
+ };
+ // Primary image IFD Interoperability tag (See JEITA CP-3451 Table 17. page 56).
+ private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
+ new ExifTag("InteroperabilityIndex", 1),
+ };
+ // IFD Thumbnail tags (See JEITA CP-3451 Table 18. page 57).
+ private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
+ new ExifTag("ThumbnailImageWidth", 256),
+ new ExifTag("ThumbnailImageLength", 257),
+ new ExifTag("BitsPerSample", 258),
+ new ExifTag("Compression", 259),
+ new ExifTag("PhotometricInterpretation", 262),
+ new ExifTag("ImageDescription", 270),
+ new ExifTag("Make", 271),
+ new ExifTag("Model", 272),
+ new ExifTag("StripOffsets", 273),
+ new ExifTag("Orientation", 274),
+ new ExifTag("SamplesPerPixel", 277),
+ new ExifTag("RowsPerStrip", 278),
+ new ExifTag("StripByteCounts", 279),
+ new ExifTag("XResolution", 282),
+ new ExifTag("YResolution", 283),
+ new ExifTag("PlanarConfiguration", 284),
+ new ExifTag("ResolutionUnit", 296),
+ new ExifTag("TransferFunction", 301),
+ new ExifTag("Software", 305),
+ new ExifTag("DateTime", 306),
+ new ExifTag("Artist", 315),
+ new ExifTag("WhitePoint", 318),
+ new ExifTag("PrimaryChromaticities", 319),
+ new ExifTag("JPEGInterchangeFormat", 513),
+ new ExifTag("JPEGInterchangeFormatLength", 514),
+ new ExifTag("YCbCrCoefficients", 529),
+ new ExifTag("YCbCrSubSampling", 530),
+ new ExifTag("YCbCrPositioning", 531),
+ new ExifTag("ReferenceBlackWhite", 532),
+ new ExifTag("Copyright", 33432),
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoIFDPointer", 34853),
+ };
+
+ // See JEITA CP-3451 Figure 5. page 9.
+ // The following values are used for indicating pointers to the other Image File Directorys.
+
+ // Indices of Exif Ifd tag groups
+ private static final int IFD_TIFF_HINT = 0;
+ private static final int IFD_EXIF_HINT = 1;
+ private static final int IFD_GPS_HINT = 2;
+ private static final int IFD_INTEROPERABILITY_HINT = 3;
+ private static final int IFD_THUMBNAIL_HINT = 4;
+ // List of Exif tag groups
+ private static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
+ IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
+ IFD_THUMBNAIL_TAGS
+ };
+ // List of tags for pointing to the other image file directory offset.
+ private static final ExifTag[] IFD_POINTER_TAGS = new ExifTag[] {
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoPointer", 34853),
+ new ExifTag("InteroperabilityIFDPointer", 40965),
+ };
+ // List of indices of the indicated tag groups according to the IFD_POINTER_TAGS
+ private static final int[] IFD_POINTER_TAG_HINTS = new int[] {
+ IFD_EXIF_HINT, IFD_GPS_HINT, IFD_INTEROPERABILITY_HINT
+ };
+ // Tags for indicating the thumbnail offset and length
+ private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
+ new ExifTag("JPEGInterchangeFormat", 513);
+ private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
+ new ExifTag("JPEGInterchangeFormatLength", 514);
+
+ // Mappings from tag number to tag name and each item represents one IFD tag group.
+ private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length];
+ // Mapping from tag name to tag number and the corresponding tag group.
+ private static final HashMap<String, Pair<Integer, Integer>> sExifTagMapForWriting
+ = new HashMap<>();
+
+ // See JPEG File Interchange Format Version 1.02.
+ // The following values are defined for handling JPEG streams. In this implementation, we are
+ // not only getting information from EXIF but also from some JPEG special segments such as
+ // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+
+ // Identifier for APP1 segment in JPEG
+ private static final byte[] IDENTIFIER_APP1 = "Exif\0\0".getBytes(Charset.forName("US-ASCII"));
+ // JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
+ // the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
+ // of frame(baseline DCT) and the image size info exists in its beginning part.
+ private static final byte MARKER = (byte) 0xff;
+ private static final byte MARKER_SOI = (byte) 0xd8;
+ private static final byte MARKER_SOF0 = (byte) 0xc0;
+ private static final byte MARKER_SOF1 = (byte) 0xc1;
+ private static final byte MARKER_SOF2 = (byte) 0xc2;
+ private static final byte MARKER_SOF3 = (byte) 0xc3;
+ private static final byte MARKER_SOF5 = (byte) 0xc5;
+ private static final byte MARKER_SOF6 = (byte) 0xc6;
+ private static final byte MARKER_SOF7 = (byte) 0xc7;
+ private static final byte MARKER_SOF9 = (byte) 0xc9;
+ private static final byte MARKER_SOF10 = (byte) 0xca;
+ private static final byte MARKER_SOF11 = (byte) 0xcb;
+ private static final byte MARKER_SOF13 = (byte) 0xcd;
+ private static final byte MARKER_SOF14 = (byte) 0xce;
+ private static final byte MARKER_SOF15 = (byte) 0xcf;
+ private static final byte MARKER_SOS = (byte) 0xda;
+ private static final byte MARKER_APP1 = (byte) 0xe1;
+ private static final byte MARKER_COM = (byte) 0xfe;
+ private static final byte MARKER_EOI = (byte) 0xd9;
+
static {
- System.loadLibrary("jhead_jni");
System.loadLibrary("media_jni");
initRawNative();
-
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ // Build up the hash tables to look up Exif tags for reading Exif tags.
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ sExifTagMapsForReading[hint] = new HashMap();
+ for (ExifTag tag : EXIF_TAGS[hint]) {
+ sExifTagMapsForReading[hint].put(tag.number, tag.name);
+ }
+ }
+
+ // Build up the hash tables to look up Exif tags for writing Exif tags.
+ // There are some tags that have the same tag name in the different group. For that tags,
+ // Primary image TIFF IFD and Exif private IFD have a higher priority to map than the other
+ // tag groups. For the same tags, it writes one tag in the only one IFD group, which has the
+ // higher priority group.
+ for (int hint = EXIF_TAGS.length - 1; hint >= 0; --hint) {
+ for (ExifTag tag : EXIF_TAGS[hint]) {
+ sExifTagMapForWriting.put(tag.name, new Pair<>(tag.number, hint));
+ }
+ }
}
private final String mFilename;
- private final HashMap<String, String> mAttributes = new HashMap<>();
+ private final FileDescriptor mFileDescriptor;
+ private final InputStream mInputStream;
private boolean mIsRaw;
+ private final HashMap<String, String> mAttributes = new HashMap<>();
private boolean mHasThumbnail;
// The following values used for indicating a thumbnail position.
private int mThumbnailOffset;
private int mThumbnailLength;
-
- // Because the underlying implementation (jhead) uses static variables,
- // there can only be one user at a time for the native functions (and
- // they cannot keep state in the native code across function calls). We
- // use sLock to serialize the accesses.
- private static final Object sLock = new Object();
+ private byte[] mThumbnailBytes;
// Pattern to check non zero timestamp
private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
@@ -155,7 +489,35 @@
throw new IllegalArgumentException("filename cannot be null");
}
mFilename = filename;
- // First test whether a given file is a one of RAW format or not.
+ mFileDescriptor = null;
+ mInputStream = new FileInputStream(filename);
+ loadAttributes();
+ }
+
+ /**
+ * Reads Exif tags from the specified image file descriptor.
+ */
+ public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
+ if (fileDescriptor == null) {
+ throw new IllegalArgumentException("parcelFileDescriptor cannot be null");
+ }
+ mFilename = null;
+ mFileDescriptor = fileDescriptor;
+ mInputStream = new FileInputStream(fileDescriptor);
+ loadAttributes();
+ }
+
+ /**
+ * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
+ * for input streams.
+ */
+ public ExifInterface(InputStream inputStream) throws IOException {
+ if (inputStream == null) {
+ throw new IllegalArgumentException("inputStream cannot be null");
+ }
+ mFilename = null;
+ mFileDescriptor = null;
+ mInputStream = inputStream;
loadAttributes();
}
@@ -188,9 +550,9 @@
}
/**
- * Returns the double value of the specified rational tag. If there is no
- * such tag in the image file or the value cannot be parsed as double, return
- * <var>defaultValue</var>.
+ * Returns the double value of the tag that is specified as rational or contains a
+ * double-formatted value. If there is no such tag in the image file or the value cannot be
+ * parsed as double, return <var>defaultValue</var>.
*
* @param tag the name of the tag.
* @param defaultValue the value to return if the tag is not available.
@@ -200,7 +562,7 @@
if (value == null) return defaultValue;
try {
int index = value.indexOf("/");
- if (index == -1) return defaultValue;
+ if (index == -1) return Double.parseDouble(value);
double denom = Double.parseDouble(value.substring(index + 1));
if (denom == 0) return defaultValue;
double num = Double.parseDouble(value.substring(0, index));
@@ -217,6 +579,10 @@
* @param value the value of the tag.
*/
public void setAttribute(String tag, String value) {
+ if (value == null) {
+ mAttributes.remove(tag);
+ return;
+ }
mAttributes.put(tag, value);
}
@@ -231,68 +597,74 @@
* file has a thumbnail inside.
*/
private void loadAttributes() throws IOException {
- HashMap map = getRawAttributesNative(mFilename);
- mIsRaw = map != null;
- if (mIsRaw) {
- for (Object o : map.entrySet()) {
- Map.Entry entry = (Map.Entry) o;
- String attrName = (String) entry.getKey();
- String attrValue = (String) entry.getValue();
+ FileInputStream in = null;
+ try {
+ if (mFilename != null) {
+ in = new FileInputStream(mFilename);
+ }
+ if (mFileDescriptor != null) {
+ in = new FileInputStream(mFileDescriptor);
+ }
+ if (in != null) {
+ // First test whether a given file is a one of RAW format or not.
+ HashMap map = getRawAttributesNative(Os.dup(in.getFD()));
+ mIsRaw = map != null;
+ if (mIsRaw) {
+ for (Object obj : map.entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ String attrName = (String) entry.getKey();
+ String attrValue = (String) entry.getValue();
- switch (attrName) {
- case TAG_HAS_THUMBNAIL:
- mHasThumbnail = attrValue.equalsIgnoreCase("true");
- break;
- case TAG_THUMBNAIL_OFFSET:
- mThumbnailOffset = Integer.parseInt(attrValue);
- break;
- case TAG_THUMBNAIL_LENGTH:
- mThumbnailLength = Integer.parseInt(attrValue);
- break;
- default:
- mAttributes.put(attrName, attrValue);
- break;
+ switch (attrName) {
+ case TAG_HAS_THUMBNAIL:
+ mHasThumbnail = attrValue.equalsIgnoreCase("true");
+ break;
+ case TAG_THUMBNAIL_OFFSET:
+ mThumbnailOffset = Integer.parseInt(attrValue);
+ break;
+ case TAG_THUMBNAIL_LENGTH:
+ mThumbnailLength = Integer.parseInt(attrValue);
+ break;
+ default:
+ mAttributes.put(attrName, attrValue);
+ break;
+ }
+ }
+
+ if (DEBUG) {
+ printAttributes();
+ }
+ return;
}
}
- return;
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
}
- // format of string passed from native C code:
- // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
- // example:
- // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
-
- String attrStr;
- synchronized (sLock) {
- attrStr = getAttributesNative(mFilename);
- }
-
- // get count
- int ptr = attrStr.indexOf(' ');
- int count = Integer.parseInt(attrStr.substring(0, ptr));
- // skip past the space between item count and the rest of the attributes
- ++ptr;
-
- for (int i = 0; i < count; i++) {
- // extract the attribute name
- int equalPos = attrStr.indexOf('=', ptr);
- String attrName = attrStr.substring(ptr, equalPos);
- ptr = equalPos + 1; // skip past =
-
- // extract the attribute value length
- int lenPos = attrStr.indexOf(' ', ptr);
- int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
- ptr = lenPos + 1; // skip pas the space
-
- // extract the attribute value
- String attrValue = attrStr.substring(ptr, ptr + attrLen);
- ptr += attrLen;
-
- if (attrName.equals(TAG_HAS_THUMBNAIL)) {
- mHasThumbnail = attrValue.equalsIgnoreCase("true");
- } else {
- mAttributes.put(attrName, attrValue);
+ try {
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
}
+
+ getJpegAttributes(mInputStream);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(mInputStream);
+ }
+
+ if (DEBUG) {
+ printAttributes();
+ }
+ }
+
+ // Prints out attributes for debugging.
+ private void printAttributes() {
+ Log.d(TAG, "The size of tags: " + mAttributes.size());
+ for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+ Log.d(TAG, "tagName: " + entry.getKey() + ", tagValue: " + entry.getValue());
}
}
@@ -307,33 +679,63 @@
throw new UnsupportedOperationException(
"ExifInterface does not support saving attributes on RAW formats.");
}
+ if (mFileDescriptor == null && mFilename == null) {
+ throw new UnsupportedOperationException(
+ "ExifInterface does not support saving attributes for input streams.");
+ }
- // format of string passed to native C code:
- // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
- // example:
- // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
- StringBuilder sb = new StringBuilder();
- int size = mAttributes.size();
- if (mAttributes.containsKey(TAG_HAS_THUMBNAIL)) {
- --size;
- }
- sb.append(size).append(" ");
- for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
- String key = entry.getKey();
- if (key.equals(TAG_HAS_THUMBNAIL)) {
- // this is a fake attribute not saved as an exif tag
- continue;
+ // Keep the thumbnail in memory
+ mThumbnailBytes = getThumbnail();
+
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ File tempFile = null;
+ try {
+ // Move the original file to temporary file.
+ if (mFilename != null) {
+ tempFile = new File(mFilename + ".tmp");
+ File originalFile = new File(mFilename);
+ if (!originalFile.renameTo(tempFile)) {
+ throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath());
+ }
}
- String val = entry.getValue();
- sb.append(key).append("=");
- sb.append(val.length()).append(" ");
- sb.append(val);
+ if (mFileDescriptor != null) {
+ tempFile = File.createTempFile("temp", "jpg");
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ in = new FileInputStream(mFileDescriptor);
+ out = new FileOutputStream(tempFile);
+ Streams.copy(in, out);
+ }
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
+ IoUtils.closeQuietly(out);
}
- String s = sb.toString();
- synchronized (sLock) {
- saveAttributesNative(mFilename, s);
- commitChangesNative(mFilename);
+
+ in = null;
+ out = null;
+ try {
+ // Save the new file.
+ in = new FileInputStream(tempFile);
+ if (mFilename != null) {
+ out = new FileOutputStream(mFilename);
+ }
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ out = new FileOutputStream(mFileDescriptor);
+ }
+ saveJpegAttributes(in, out);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
+ IoUtils.closeQuietly(out);
+ tempFile.delete();
}
+
+ // Discard the thumbnail in memory
+ mThumbnailBytes = null;
}
/**
@@ -349,27 +751,41 @@
* {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
*/
public byte[] getThumbnail() {
- if (mIsRaw) {
- if (mHasThumbnail) {
- try (RandomAccessFile file = new RandomAccessFile(mFilename, "r")) {
- if (file.length() < mThumbnailLength + mThumbnailOffset) {
- throw new IOException("Corrupted image.");
- }
- file.seek(mThumbnailOffset);
-
- byte[] buffer = new byte[mThumbnailLength];
- file.readFully(buffer);
- return buffer;
- } catch (IOException e) {
- // Couldn't get a thumbnail image.
- }
- }
+ if (!mHasThumbnail) {
return null;
}
-
- synchronized (sLock) {
- return getThumbnailNative(mFilename);
+ if (mThumbnailBytes != null) {
+ return mThumbnailBytes;
}
+
+ // Read the thumbnail.
+ FileInputStream in = null;
+ try {
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ in = new FileInputStream(mFileDescriptor);
+ }
+ if (mFilename != null) {
+ in = new FileInputStream(mFilename);
+ }
+ if (in == null) {
+ // Should not be reached this.
+ throw new FileNotFoundException();
+ }
+ if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
+ throw new IOException("Corrupted image");
+ }
+ byte[] buffer = new byte[mThumbnailLength];
+ if (in.read(buffer) != mThumbnailLength) {
+ throw new IOException("Corrupted image");
+ }
+ return buffer;
+ } catch (IOException | ErrnoException e) {
+ // Couldn't get a thumbnail image.
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ return null;
}
/**
@@ -381,16 +797,10 @@
* @hide
*/
public long[] getThumbnailRange() {
- if (mIsRaw) {
- long[] range = new long[2];
- range[0] = mThumbnailOffset;
- range[1] = mThumbnailLength;
- return range;
- }
-
- synchronized (sLock) {
- return getThumbnailRangeNative(mFilename);
- }
+ long[] range = new long[2];
+ range[0] = mThumbnailOffset;
+ range[1] = mThumbnailLength;
+ return range;
}
/**
@@ -399,10 +809,10 @@
* Exif tags are not available.
*/
public boolean getLatLong(float output[]) {
- String latValue = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE);
- String latRef = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE_REF);
- String lngValue = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE);
- String lngRef = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
+ String latValue = mAttributes.get(TAG_GPS_LATITUDE);
+ String latRef = mAttributes.get(TAG_GPS_LATITUDE_REF);
+ String lngValue = mAttributes.get(TAG_GPS_LONGITUDE);
+ String lngRef = mAttributes.get(TAG_GPS_LONGITUDE_REF);
if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
try {
@@ -428,7 +838,7 @@
int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
if (altitude >= 0 && ref >= 0) {
- return (double) (altitude * ((ref == 1) ? -1 : 1));
+ return (altitude * ((ref == 1) ? -1 : 1));
} else {
return defaultValue;
}
@@ -461,6 +871,7 @@
}
msecs += sub;
} catch (NumberFormatException e) {
+ // Ignored
}
}
return msecs;
@@ -493,8 +904,7 @@
}
}
- private static float convertRationalLatLonToFloat(
- String rationalString, String ref) {
+ private static float convertRationalLatLonToFloat(String rationalString, String ref) {
try {
String [] parts = rationalString.split(",");
@@ -522,22 +932,1062 @@
}
}
- // JNI methods for JPEG.
- private static native boolean appendThumbnailNative(String fileName,
- String thumbnailFileName);
+ // Loads EXIF attributes from a JPEG input stream.
+ private void getJpegAttributes(InputStream inputStream) throws IOException {
+ // See JPEG File Interchange Format Specification page 5.
+ if (DEBUG) {
+ Log.d(TAG, "getJpegAttributes starting with: " + inputStream);
+ }
+ DataInputStream dataInputStream = new DataInputStream(inputStream);
+ byte marker;
+ int bytesRead = 0;
+ ++bytesRead;
+ if ((marker = dataInputStream.readByte()) != MARKER) {
+ throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+ }
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER_SOI) {
+ throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+ }
+ while (true) {
+ ++bytesRead;
+ marker = dataInputStream.readByte();
+ if (marker != MARKER) {
+ throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
+ }
+ ++bytesRead;
+ marker = dataInputStream.readByte();
+ if (DEBUG) {
+ Log.d(TAG, "Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
+ }
- private static native void saveAttributesNative(String fileName,
- String compressedAttributes);
+ // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
+ // the image data will terminate right after.
+ if (marker == MARKER_EOI || marker == MARKER_SOS) {
+ break;
+ }
+ bytesRead += 2;
+ int length = dataInputStream.readUnsignedShort() - 2;
+ if (length < 0)
+ throw new IOException("Invalid length");
+ bytesRead += length;
+ switch (marker) {
+ case MARKER_APP1: {
+ if (DEBUG) {
+ Log.d(TAG, "MARKER_APP1");
+ }
+ bytesRead -= length;
+ if (length < 6) {
+ throw new IOException("Invalid exif");
+ }
+ byte[] identifier = new byte[6];
+ if (inputStream.read(identifier) != 6) {
+ throw new IOException("Invalid exif");
+ }
+ if (!Arrays.equals(identifier, IDENTIFIER_APP1)) {
+ throw new IOException("Invalid app1 identifier");
+ }
+ bytesRead += 6;
+ length -= 6;
+ if (length <= 0) {
+ throw new IOException("Invalid exif");
+ }
+ byte[] bytes = new byte[length];
+ if (dataInputStream.read(bytes) != length) {
+ throw new IOException("Invalid exif");
+ }
+ readExifSegment(bytes, bytesRead);
+ bytesRead += length;
+ length = 0;
+ break;
+ }
- private static native String getAttributesNative(String fileName);
+ case MARKER_COM: {
+ byte[] bytes = new byte[length];
+ if (dataInputStream.read(bytes) != length) {
+ throw new IOException("Invalid exif");
+ }
+ mAttributes.put("UserComment",
+ new String(bytes, Charset.forName("US-ASCII")));
+ break;
+ }
- private static native void commitChangesNative(String fileName);
+ case MARKER_SOF0:
+ case MARKER_SOF1:
+ case MARKER_SOF2:
+ case MARKER_SOF3:
+ case MARKER_SOF5:
+ case MARKER_SOF6:
+ case MARKER_SOF7:
+ case MARKER_SOF9:
+ case MARKER_SOF10:
+ case MARKER_SOF11:
+ case MARKER_SOF13:
+ case MARKER_SOF14:
+ case MARKER_SOF15: {
+ dataInputStream.skipBytes(1);
+ mAttributes.put("ImageLength",
+ String.valueOf(dataInputStream.readUnsignedShort()));
+ mAttributes.put("ImageWidth",
+ String.valueOf(dataInputStream.readUnsignedShort()));
+ length -= 5;
+ break;
+ }
- private static native byte[] getThumbnailNative(String fileName);
+ default: {
+ break;
+ }
+ }
+ if (length < 0) {
+ throw new IOException("Invalid length");
+ }
+ dataInputStream.skipBytes(length);
+ }
+ }
- private static native long[] getThumbnailRangeNative(String fileName);
+ // Stores a new JPEG image with EXIF attributes into a given output stream.
+ private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
+ throws IOException {
+ // See JPEG File Interchange Format Specification page 5.
+ if (DEBUG) {
+ Log.d(TAG, "saveJpegAttributes starting with (inputStream: " + inputStream
+ + ", outputStream: " + outputStream + ")");
+ }
+ DataInputStream dataInputStream = new DataInputStream(inputStream);
+ ExifDataOutputStream dataOutputStream = new ExifDataOutputStream(outputStream);
+ int bytesRead = 0;
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER);
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER_SOI) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER_SOI);
+
+ byte[] bytes = new byte[4096];
+
+ while (true) {
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER);
+ ++bytesRead;
+ byte marker = dataInputStream.readByte();
+ dataOutputStream.writeByte(marker);
+ switch (marker) {
+ case MARKER_APP1: {
+ // Rewrite EXIF segment
+ int length = dataInputStream.readUnsignedShort() - 2;
+ if (length < 0)
+ throw new IOException("Invalid length");
+ bytesRead += 2;
+ int read;
+ while ((read = dataInputStream.read(
+ bytes, 0, Math.min(length, bytes.length))) > 0) {
+ length -= read;
+ }
+ bytesRead += length;
+ writeExifSegment(dataOutputStream, bytesRead);
+ break;
+ }
+ case MARKER_EOI:
+ case MARKER_SOS: {
+ // Copy all the remaining data
+ Streams.copy(dataInputStream, dataOutputStream);
+ return;
+ }
+ default: {
+ // Copy JPEG segment
+ int length = dataInputStream.readUnsignedShort();
+ dataOutputStream.writeUnsignedShort(length);
+ if (length < 0)
+ throw new IOException("Invalid length");
+ length -= 2;
+ bytesRead += 2;
+ int read;
+ while ((read = dataInputStream.read(
+ bytes, 0, Math.min(length, bytes.length))) > 0) {
+ dataOutputStream.write(bytes, 0, read);
+ length -= read;
+ }
+ bytesRead += length;
+ break;
+ }
+ }
+ }
+ }
+
+ // Reads the given EXIF byte area and save its tag data into attributes.
+ private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning) throws IOException {
+ // Parse TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+ ByteOrderAwarenessDataInputStream dataInputStream =
+ new ByteOrderAwarenessDataInputStream(exifBytes);
+
+ // Read byte align
+ short byteOrder = dataInputStream.readShort();
+ switch (byteOrder) {
+ case BYTE_ALIGN_II:
+ if (DEBUG) {
+ Log.d(TAG, "readExifSegment: Byte Align II");
+ }
+ dataInputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ break;
+ case BYTE_ALIGN_MM:
+ if (DEBUG) {
+ Log.d(TAG, "readExifSegment: Byte Align MM");
+ }
+ dataInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ break;
+ default:
+ throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
+ }
+
+ int startCode = dataInputStream.readUnsignedShort();
+ if (startCode != 0x2a) {
+ throw new IOException("Invalid exif start: " + Integer.toHexString(startCode));
+ }
+
+ // Read first ifd offset
+ long firstIfdOffset = dataInputStream.readUnsignedInt();
+ if (firstIfdOffset < 8 || firstIfdOffset >= exifBytes.length) {
+ throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
+ }
+ firstIfdOffset -= 8;
+ if (firstIfdOffset > 0) {
+ if (dataInputStream.skip(firstIfdOffset) != firstIfdOffset)
+ throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset);
+ }
+
+ // Read primary image TIFF image file directory.
+ readImageFileDirectory(dataInputStream, IFD_TIFF_HINT);
+
+ // Process thumbnail.
+ try {
+ int jpegInterchangeFormat = Integer.parseInt(
+ mAttributes.get(JPEG_INTERCHANGE_FORMAT_TAG.name));
+ int jpegInterchangeFormatLength = Integer.parseInt(
+ mAttributes.get(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name));
+ // The following code limits the size of thumbnail size not to overflow EXIF data area.
+ jpegInterchangeFormatLength = Math.min(jpegInterchangeFormat
+ + jpegInterchangeFormatLength, exifOffsetFromBeginning + exifBytes.length)
+ - jpegInterchangeFormat;
+ if (jpegInterchangeFormat > 0 && jpegInterchangeFormatLength > 0) {
+ mHasThumbnail = true;
+ mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat;
+ mThumbnailLength = jpegInterchangeFormatLength;
+
+ // Do not store a thumbnail in memory if the given input can be re-read.
+ if (mFileDescriptor == null && mFilename == null) {
+ byte[] thumbnailBytes = new byte[jpegInterchangeFormatLength];
+ dataInputStream.seek(jpegInterchangeFormat);
+ dataInputStream.readFully(thumbnailBytes);
+ mThumbnailBytes = thumbnailBytes;
+
+ if (DEBUG) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(
+ thumbnailBytes, 0, thumbnailBytes.length);
+ Log.d(TAG, "Thumbnail offset: " + mThumbnailOffset + ", length: "
+ + mThumbnailLength + ", width: " + bitmap.getWidth() + ", height: "
+ + bitmap.getHeight());
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Ignored the corrupted image.
+ }
+
+ // For compatibility, keep data formats as follows.
+ convertToInt(TAG_IMAGE_WIDTH);
+ convertToInt(TAG_IMAGE_LENGTH);
+ convertToInt(TAG_ORIENTATION);
+ convertToInt(TAG_FLASH);
+ convertToRational(TAG_FOCAL_LENGTH);
+ convertToDouble(TAG_DIGITAL_ZOOM_RATIO);
+ convertToDouble(TAG_EXPOSURE_TIME);
+ convertToDouble(TAG_APERTURE);
+ convertToDouble(TAG_SUBJECT_DISTANCE);
+ convertToInt(TAG_ISO);
+ convertToDouble(TAG_EXPOSURE_BIAS_VALUE);
+ convertToInt(TAG_WHITE_BALANCE);
+ convertToInt(TAG_LIGHT_SOURCE);
+ convertToInt(TAG_METERING_MODE);
+ convertToInt(TAG_EXPOSURE_PROGRAM);
+ convertToInt(TAG_EXPOSURE_MODE);
+ convertToRational(TAG_GPS_ALTITUDE);
+ convertToInt(TAG_GPS_ALTITUDE_REF);
+ convertToRational(TAG_GPS_LONGITUDE);
+ convertToRational(TAG_GPS_LATITUDE);
+ convertToTimetamp(TAG_GPS_TIMESTAMP);
+
+ // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag.
+ String valueOfDateTimeOriginal = mAttributes.get("DateTimeOriginal");
+ if (valueOfDateTimeOriginal != null) {
+ mAttributes.put(TAG_DATETIME, valueOfDateTimeOriginal);
+ }
+
+ // Add the default value.
+ if (!mAttributes.containsKey(TAG_IMAGE_WIDTH)) {
+ mAttributes.put(TAG_IMAGE_WIDTH, "0");
+ }
+ if (!mAttributes.containsKey(TAG_IMAGE_LENGTH)) {
+ mAttributes.put(TAG_IMAGE_LENGTH, "0");
+ }
+ if (!mAttributes.containsKey(TAG_ORIENTATION)) {
+ mAttributes.put(TAG_ORIENTATION, "0");
+ }
+ if (!mAttributes.containsKey(TAG_LIGHT_SOURCE)) {
+ mAttributes.put(TAG_LIGHT_SOURCE, "0");
+ }
+ }
+
+ // Converts the tag value to timestamp; Otherwise deletes the given tag.
+ private void convertToTimetamp(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ String[] components = entryValue.split(",");
+ if (dataFormat == IFD_FORMAT_SRATIONAL && components.length == 3) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(":");
+ }
+ String[] rationalNumber = component.split("/");
+ int numerator = Integer.parseInt(rationalNumber[0]);
+ int denominator = Integer.parseInt(rationalNumber[1]);
+ if (denominator == 0) {
+ numerator = 0;
+ denominator = 1;
+ }
+ int value = numerator / denominator;
+ stringBuilder.append(String.format("%02d", value));
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ } else if (dataFormat != IFD_FORMAT_STRING) {
+ mAttributes.remove(tagName);
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in double type; Otherwise try to convert it to
+ // double type or delete it.
+ private void convertToDouble(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ switch (dataFormat) {
+ case IFD_FORMAT_SRATIONAL: {
+ StringBuilder stringBuilder = new StringBuilder();
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(",");
+ }
+ String[] rationalNumber = component.split("/");
+ int numerator = Integer.parseInt(rationalNumber[0]);
+ int denominator = Integer.parseInt(rationalNumber[1]);
+ if (denominator == 0) {
+ numerator = 0;
+ denominator = 1;
+ }
+ stringBuilder.append((double) numerator / denominator);
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ break;
+ }
+ case IFD_FORMAT_DOUBLE:
+ // Keep it as is.
+ break;
+ default:
+ mAttributes.remove(tagName);
+ break;
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+ private void convertToRational(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ case IFD_FORMAT_DOUBLE: {
+ StringBuilder stringBuilder = new StringBuilder();
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(",");
+ }
+ double doubleValue = Double.parseDouble(component);
+ stringBuilder.append((int) (doubleValue * 10000.0)).append("/").append(10000);
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ break;
+ }
+ case IFD_FORMAT_SRATIONAL:
+ // Keep it as is.
+ break;
+ default:
+ mAttributes.remove(tagName);
+ break;
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+ private void convertToInt(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ if (dataFormat != IFD_FORMAT_SLONG) {
+ mAttributes.remove(tagName);
+ }
+ }
+
+ // Reads image file directory, which is a tag group in EXIF.
+ private void readImageFileDirectory(ByteOrderAwarenessDataInputStream dataInputStream, int hint)
+ throws IOException {
+ // See JEITA CP-3451 Figure 5. page 9.
+ short numberOfDirectoryEntry = dataInputStream.readShort();
+
+ if (DEBUG) {
+ Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry);
+ }
+
+ for (short i = 0; i < numberOfDirectoryEntry; ++i) {
+ int tagNumber = dataInputStream.readUnsignedShort();
+ int dataFormat = dataInputStream.readUnsignedShort();
+ int numberOfComponents = dataInputStream.readInt();
+ long nextEntryOffset = dataInputStream.peek() + 4; // next four bytes is for data
+ // offset or value.
+
+ if (DEBUG) {
+ Log.d(TAG, String.format("tagNumber: %d, dataFormat: %d, numberOfComponents: %d",
+ tagNumber, dataFormat, numberOfComponents));
+ }
+
+ // Read a value from data field or seek to the value offset which is stored in data
+ // field if the size of the entry value is bigger than 4.
+ int byteCount = numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+ if (byteCount > 4) {
+ long offset = dataInputStream.readUnsignedInt();
+ if (DEBUG) {
+ Log.d(TAG, "seek to data offset: " + offset);
+ }
+ dataInputStream.seek(offset);
+ }
+
+ // Look up a corresponding tag from tag number
+ String tagName = (String) sExifTagMapsForReading[hint].get(tagNumber);
+ // Skip if the parsed tag number is not defined.
+ if (tagName == null) {
+ dataInputStream.seek(nextEntryOffset);
+ continue;
+ }
+
+ // Recursively parse IFD when a IFD pointer tag appears.
+ int innerIfdHint = getIfdHintFromTagNumber(tagNumber);
+ if (innerIfdHint >= 0) {
+ long offset = -1L;
+ // Get offset from data field
+ switch (dataFormat) {
+ case IFD_FORMAT_USHORT: {
+ offset = dataInputStream.readUnsignedShort();
+ break;
+ }
+ case IFD_FORMAT_SSHORT: {
+ offset = dataInputStream.readShort();
+ break;
+ }
+ case IFD_FORMAT_ULONG: {
+ offset = dataInputStream.readUnsignedInt();
+ break;
+ }
+ case IFD_FORMAT_SLONG: {
+ offset = dataInputStream.readInt();
+ break;
+ }
+ default: {
+ // Nothing to do
+ break;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("Offset: %d, tagName: %s", offset, tagName));
+ }
+ if (offset > 0L) {
+ dataInputStream.seek(offset);
+ readImageFileDirectory(dataInputStream, innerIfdHint);
+ }
+
+ dataInputStream.seek(nextEntryOffset);
+ continue;
+ }
+
+ if (numberOfComponents == 1 || dataFormat == IFD_FORMAT_STRING
+ || dataFormat == IFD_FORMAT_UNDEFINED) {
+ String entryValue = readExifEntryValue(
+ dataInputStream, dataFormat, numberOfComponents);
+ if (entryValue != null) {
+ mAttributes.put(tagName, entryValue);
+ }
+ } else {
+ StringBuilder entryValueBuilder = new StringBuilder();
+ for (int c = 0; c < numberOfComponents; ++c) {
+ if (entryValueBuilder.length() > 0) {
+ entryValueBuilder.append(",");
+ }
+ entryValueBuilder.append(readExifEntryValue(
+ dataInputStream, dataFormat, numberOfComponents));
+ }
+ mAttributes.put(tagName, entryValueBuilder.toString());
+ }
+
+ if (dataInputStream.peek() != nextEntryOffset) {
+ dataInputStream.seek(nextEntryOffset);
+ }
+ }
+
+ long nextIfdOffset = dataInputStream.readUnsignedInt();
+ if (DEBUG) {
+ Log.d(TAG, String.format("nextIfdOffset: %d", nextIfdOffset));
+ }
+ // The next IFD offset needs to be bigger than 8 since the first IFD offset is at least 8.
+ if (nextIfdOffset > 8) {
+ dataInputStream.seek(nextIfdOffset);
+ readImageFileDirectory(dataInputStream, IFD_THUMBNAIL_HINT);
+ }
+ }
+
+ // Reads a value from where the entry value are stored.
+ private String readExifEntryValue(ByteOrderAwarenessDataInputStream dataInputStream,
+ int dataFormat, int numberOfComponents) throws IOException {
+ // See TIFF 6.0 spec Types. page 15.
+ switch (dataFormat) {
+ case IFD_FORMAT_BYTE: {
+ return String.valueOf(dataInputStream.readByte());
+ }
+ case IFD_FORMAT_SBYTE: {
+ return String.valueOf(dataInputStream.readByte() & 0xff);
+ }
+ case IFD_FORMAT_USHORT: {
+ return String.valueOf(dataInputStream.readUnsignedShort());
+ }
+ case IFD_FORMAT_SSHORT: {
+ return String.valueOf(dataInputStream.readUnsignedInt());
+ }
+ case IFD_FORMAT_ULONG: {
+ return String.valueOf(dataInputStream.readInt());
+ }
+ case IFD_FORMAT_SLONG: {
+ return String.valueOf(dataInputStream.readInt());
+ }
+ case IFD_FORMAT_URATIONAL:
+ case IFD_FORMAT_SRATIONAL: {
+ int numerator = dataInputStream.readInt();
+ int denominator = dataInputStream.readInt();
+ return numerator + "/" + denominator;
+ }
+ case IFD_FORMAT_SINGLE: {
+ return String.valueOf(dataInputStream.readFloat());
+ }
+ case IFD_FORMAT_DOUBLE: {
+ return String.valueOf(dataInputStream.readDouble());
+ }
+ case IFD_FORMAT_UNDEFINED: // Usually UNDEFINED format is ASCII.
+ case IFD_FORMAT_STRING: {
+ byte[] bytes = new byte[numberOfComponents];
+ dataInputStream.readFully(bytes);
+ int index = 0;
+ if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
+ boolean same = true;
+ for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
+ if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
+ same = false;
+ break;
+ }
+ }
+ if (same) {
+ index = EXIF_ASCII_PREFIX.length;
+ }
+ }
+
+ StringBuilder stringBuilder = new StringBuilder();
+ while (true) {
+ int ch = bytes[index];
+ if (ch < 0)
+ throw new EOFException();
+ if (ch == 0)
+ break;
+ if (ch >= 32)
+ stringBuilder.append((char) ch);
+ else
+ stringBuilder.append('?');
+ ++index;
+ if (index == numberOfComponents)
+ break;
+ }
+ return stringBuilder.toString();
+ }
+ default: {
+ // Nothing to do
+ return null;
+ }
+ }
+ }
+
+ // Gets the corresponding IFD group index of the given tag number for writing Exif Tags.
+ private static int getIfdHintFromTagNumber(int tagNumber) {
+ for (int i = 0; i < IFD_POINTER_TAG_HINTS.length; ++i) {
+ if (IFD_POINTER_TAGS[i].number == tagNumber)
+ return IFD_POINTER_TAG_HINTS[i];
+ }
+ return -1;
+ }
+
+ // Writes an Exif segment into the given output stream.
+ private int writeExifSegment(ExifDataOutputStream dataOutputStream, int exifOffsetFromBeginning)
+ throws IOException {
+ // The following variables are for calculating each IFD tag group size in bytes.
+ int[] ifdOffsets = new int[EXIF_TAGS.length];
+ int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+ // Maps to store tags per IFD tag group
+ HashMap[] ifdTags = new HashMap[EXIF_TAGS.length];
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ ifdTags[i] = new HashMap();
+ }
+
+ // Remove IFD pointer tags (we'll re-add it later.)
+ for (ExifTag tag : IFD_POINTER_TAGS) {
+ mAttributes.remove(tag.name);
+ }
+
+ // Assign tags to the corresponding group
+ for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+ Pair<Integer, Integer> pair = sExifTagMapForWriting.get(entry.getKey());
+ if (pair != null) {
+ int tagNumber = pair.first;
+ int hint = pair.second;
+ ifdTags[hint].put(tagNumber, entry.getValue());
+ }
+ }
+
+ // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+ // offset when there is one or more tags in the thumbnail IFD.
+ if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+ ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number, "0");
+ }
+ if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number, "0");
+ }
+ if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number, "0");
+ }
+ if (mHasThumbnail) {
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number, "0");
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+ String.valueOf(mThumbnailLength));
+ }
+
+ // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+ // value which has a bigger size than 4 bytes.
+ for (int i = 0; i < 5; ++i) {
+ int sum = 0;
+ for (Object entry : ifdTags[i].entrySet()) {
+ String entryValue = (String) ((Map.Entry) entry).getValue();
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int size = getSizeOfExifEntryValue(dataFormat, entryValue);
+ if (size > 4) {
+ sum += size;
+ }
+ }
+ ifdDataSizes[i] += sum;
+ }
+
+ // Calculate IFD offsets.
+ int position = 8;
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ if (!ifdTags[hint].isEmpty()) {
+ ifdOffsets[hint] = position;
+ position += 2 + ifdTags[hint].size() * 12 + 4 + ifdDataSizes[hint];
+ }
+ }
+ if (mHasThumbnail) {
+ int thumbnailOffset = position;
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number,
+ String.valueOf(thumbnailOffset));
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+ String.valueOf(mThumbnailLength));
+ mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
+ position += mThumbnailLength;
+ }
+
+ // Calculate the total size
+ int totalSize = position + 8; // eight bytes is for header part.
+ if (DEBUG) {
+ Log.d(TAG, "totalSize length: " + totalSize);
+ for (int i = 0; i < 5; ++i) {
+ Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
+ i, ifdOffsets[i], ifdTags[i].size(), ifdDataSizes[i]));
+ }
+ }
+
+ // Update IFD pointer tags with the calculated offsets.
+ if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number,
+ String.valueOf(ifdOffsets[IFD_EXIF_HINT]));
+ }
+ if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number,
+ String.valueOf(ifdOffsets[IFD_GPS_HINT]));
+ }
+ if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+ ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number,
+ String.valueOf(ifdOffsets[IFD_INTEROPERABILITY_HINT]));
+ }
+
+ // Write TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+ dataOutputStream.writeUnsignedShort(totalSize);
+ dataOutputStream.write(IDENTIFIER_APP1);
+ dataOutputStream.writeShort(BYTE_ALIGN_MM);
+ dataOutputStream.writeUnsignedShort(0x2a);
+ dataOutputStream.writeUnsignedInt(8);
+
+ // Write IFD groups. See JEITA CP-3451C Figure 7. page 12.
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ if (!ifdTags[hint].isEmpty()) {
+ // See JEITA CP-3451C 4.6.2 IFD structure. page 13.
+ // Write entry count
+ dataOutputStream.writeUnsignedShort(ifdTags[hint].size());
+
+ // Write entry info
+ int dataOffset = ifdOffsets[hint] + 2 + ifdTags[hint].size() * 12 + 4;
+ for (Object obj : ifdTags[hint].entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ int tagNumber = (int) entry.getKey();
+ String entryValue = (String) entry.getValue();
+
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int numberOfComponents = getNumberOfComponentsInExifEntryValue(dataFormat,
+ entryValue);
+ int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+
+ dataOutputStream.writeUnsignedShort(tagNumber);
+ dataOutputStream.writeUnsignedShort(dataFormat);
+ dataOutputStream.writeInt(numberOfComponents);
+ if (byteCount > 4) {
+ dataOutputStream.writeUnsignedInt(dataOffset);
+ dataOffset += byteCount;
+ } else {
+ int bytesWritten = writeExifEntryValue(dataOutputStream, entryValue);
+ // Fill zero up to 4 bytes
+ if (bytesWritten < 4) {
+ for (int i = bytesWritten; i < 4; ++i) {
+ dataOutputStream.write(0);
+ }
+ }
+ }
+ }
+
+ // Write the next offset. It writes the offset of thumbnail IFD if there is one or
+ // more tags in the thumbnail IFD when the current IFD is the primary image TIFF
+ // IFD; Otherwise 0.
+ if (hint == 0 && !ifdTags[IFD_THUMBNAIL_HINT].isEmpty()) {
+ dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_THUMBNAIL_HINT]);
+ } else {
+ dataOutputStream.writeUnsignedInt(0);
+ }
+
+ // Write values of data field exceeding 4 bytes after the next offset.
+ for (Object obj : ifdTags[hint].entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ String entryValue = (String) entry.getValue();
+
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+ if (byteCount > 4) {
+ writeExifEntryValue(dataOutputStream, entryValue);
+ }
+ }
+ }
+ }
+
+ // Write thumbnail
+ if (mHasThumbnail) {
+ dataOutputStream.write(getThumbnail());
+ }
+
+ return totalSize;
+ }
+
+ // Writes EXIF entry value and its entry value type will be automatically determined.
+ private static int writeExifEntryValue(ExifDataOutputStream dataOutputStream, String entryValue)
+ throws IOException {
+ int bytesWritten = 0;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+
+ // Values can be composed of several components. Each component is separated by char ','.
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ dataOutputStream.writeInt(Integer.parseInt(component));
+ bytesWritten += 4;
+ break;
+ case IFD_FORMAT_DOUBLE:
+ dataOutputStream.writeDouble(Double.parseDouble(component));
+ bytesWritten += 8;
+ break;
+ case IFD_FORMAT_STRING:
+ byte[] asciiArray = (component + '\0').getBytes(Charset.forName("US-ASCII"));
+ dataOutputStream.write(asciiArray);
+ bytesWritten += asciiArray.length;
+ break;
+ case IFD_FORMAT_SRATIONAL:
+ String[] rationalNumber = component.split("/");
+ dataOutputStream.writeInt(Integer.parseInt(rationalNumber[0]));
+ dataOutputStream.writeInt(Integer.parseInt(rationalNumber[1]));
+ bytesWritten += 8;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ return bytesWritten;
+ }
+
+ // Determines the data format of EXIF entry value.
+ private static int getDataFormatOfExifEntryValue(String entryValue) {
+ // See TIFF 6.0 spec Types. page 15.
+ // Take the first component if there are more than one component.
+ if (entryValue.contains(",")) {
+ entryValue = entryValue.split(",")[0];
+ }
+
+ if (entryValue.contains("/")) {
+ return IFD_FORMAT_SRATIONAL;
+ }
+ try {
+ Integer.parseInt(entryValue);
+ return IFD_FORMAT_SLONG;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ try {
+ Double.parseDouble(entryValue);
+ return IFD_FORMAT_DOUBLE;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ return IFD_FORMAT_STRING;
+ }
+
+ // Determines the size of EXIF entry value.
+ private static int getSizeOfExifEntryValue(int dataFormat, String entryValue) {
+ // See TIFF 6.0 spec Types page 15.
+ int bytesEstimated = 0;
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ bytesEstimated += 4;
+ break;
+ case IFD_FORMAT_DOUBLE:
+ bytesEstimated += 8;
+ break;
+ case IFD_FORMAT_STRING:
+ bytesEstimated
+ += (component + '\0').getBytes(Charset.forName("US-ASCII")).length;
+ break;
+ case IFD_FORMAT_SRATIONAL:
+ bytesEstimated += 8;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ return bytesEstimated;
+ }
+
+ // Determines the number of components of EXIF entry value.
+ private static int getNumberOfComponentsInExifEntryValue(int dataFormat, String entryValue) {
+ if (dataFormat == IFD_FORMAT_STRING) {
+ return (entryValue + '\0').getBytes(Charset.forName("US-ASCII")).length;
+ }
+ int count = 1;
+ for (int i = 0; i < entryValue.length(); ++i) {
+ if (entryValue.charAt(i) == ',') {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ // An input stream to parse EXIF data area, which can be written in either little or big endian
+ // order.
+ private static class ByteOrderAwarenessDataInputStream extends ByteArrayInputStream {
+ private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
+ private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
+
+ private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+ private final long mLength;
+ private long mPosition;
+
+ public ByteOrderAwarenessDataInputStream(byte[] bytes) {
+ super(bytes);
+ mLength = bytes.length;
+ mPosition = 0L;
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ mByteOrder = byteOrder;
+ }
+
+ public void seek(long byteCount) throws IOException {
+ mPosition = 0L;
+ reset();
+ if (skip(byteCount) != byteCount)
+ throw new IOException("Couldn't seek up to the byteCount");
+ }
+
+ public long peek() {
+ return mPosition;
+ }
+
+ public void readFully(byte[] buffer) throws IOException {
+ mPosition += buffer.length;
+ if (mPosition > mLength)
+ throw new EOFException();
+ if (super.read(buffer, 0, buffer.length) != buffer.length) {
+ throw new IOException("Couldn't read up to the length of buffer");
+ }
+ }
+
+ public byte readByte() throws IOException {
+ ++mPosition;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch = super.read();
+ if (ch < 0)
+ throw new EOFException();
+ return (byte) ch;
+ }
+
+ public short readShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (short) ((ch2 << 8) + (ch1));
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (short) ((ch1 << 8) + (ch2));
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public int readInt() throws IOException {
+ mPosition += 4;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ int ch3 = super.read();
+ int ch4 = super.read();
+ if ((ch1 | ch2 | ch3 | ch4) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ @Override
+ public long skip(long byteCount) {
+ long skipped = super.skip(Math.min(byteCount, mLength - mPosition));
+ mPosition += skipped;
+ return skipped;
+ }
+
+ public int readUnsignedShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch2 << 8) + (ch1));
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 8) + (ch2));
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ public long readLong() throws IOException {
+ mPosition += 8;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ int ch3 = super.read();
+ int ch4 = super.read();
+ int ch5 = super.read();
+ int ch6 = super.read();
+ int ch7 = super.read();
+ int ch8 = super.read();
+ if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+ + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+ + ((long) ch2 << 8) + (long) ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+ + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+ + ((long) ch7 << 8) + (long) ch8);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public float readFloat() throws IOException {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ public double readDouble() throws IOException {
+ return Double.longBitsToDouble(readLong());
+ }
+ }
+
+ // An output stream to write EXIF data area, that will be written in big endian byte order.
+ private static class ExifDataOutputStream extends DataOutputStream {
+ public ExifDataOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public void writeUnsignedShort(int val) throws IOException {
+ writeShort((short) val);
+ }
+
+ public void writeUnsignedInt(long val) throws IOException {
+ writeInt((int) val);
+ }
+ }
// JNI methods for RAW formats.
private static native void initRawNative();
- private static native HashMap getRawAttributesNative(String filename);
+ private static native HashMap getRawAttributesNative(FileDescriptor fileDescriptor);
}
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index a326f6f..fa9c48c 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -43,14 +43,10 @@
libcamera_client \
libmtp \
libusbhost \
- libjhead \
libexif \
libpiex \
libstagefright_amrnb_common
-LOCAL_REQUIRED_MODULES := \
- libjhead_jni
-
LOCAL_STATIC_LIBRARIES := \
libstagefright_amrnbenc
diff --git a/media/jni/android_media_ExifInterface.cpp b/media/jni/android_media_ExifInterface.cpp
index ba38569..a1fcb07 100644
--- a/media/jni/android_media_ExifInterface.cpp
+++ b/media/jni/android_media_ExifInterface.cpp
@@ -83,19 +83,18 @@
}
static jobject ExifInterface_getRawMetadata(
- JNIEnv* env, jclass /* clazz */, jstring jfilename) {
- const char* filenameChars = env->GetStringUTFChars(jfilename, NULL);
- if (filenameChars == NULL) {
+ JNIEnv* env, jclass /* clazz */, jobject jfileDescriptor) {
+ int fd = jniGetFDFromFileDescriptor(env, jfileDescriptor);
+ if (fd < 0) {
+ ALOGI("Invalid file descriptor");
return NULL;
}
- String8 filename(filenameChars);
- env->ReleaseStringUTFChars(jfilename, filenameChars);
piex::PreviewImageData image_data;
- std::unique_ptr<FileStream> stream(new FileStream(filename));
+ std::unique_ptr<FileStream> stream(new FileStream(fd));
- if (!GetExifFromRawImage(stream.get(), filename, image_data)) {
- ALOGI("Raw image not detected: %s", filename.string());
+ if (!GetExifFromRawImage(stream.get(), String8("[file descriptor]"), image_data)) {
+ ALOGI("Raw image not detected");
return NULL;
}
@@ -263,7 +262,7 @@
static JNINativeMethod gMethods[] = {
{ "initRawNative", "()V", (void *)ExifInterface_initRaw },
- { "getRawAttributesNative", "(Ljava/lang/String;)Ljava/util/HashMap;",
+ { "getRawAttributesNative", "(Ljava/io/FileDescriptor;)Ljava/util/HashMap;",
(void*)ExifInterface_getRawMetadata },
};
diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp
index c08a5e3..9c4f7c4 100644
--- a/media/jni/android_media_Utils.cpp
+++ b/media/jni/android_media_Utils.cpp
@@ -28,6 +28,19 @@
namespace android {
+FileStream::FileStream(const int fd)
+ : mPosition(0),
+ mSize(0) {
+ mFile = fdopen(fd, "r");
+ if (mFile == NULL) {
+ return;
+ }
+ // Get the size.
+ fseek(mFile, 0l, SEEK_END);
+ mSize = ftell(mFile);
+ fseek(mFile, 0l, SEEK_SET);
+}
+
FileStream::FileStream(const String8 filename)
: mPosition(0),
mSize(0) {
diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h
index 762c904..a30e1be 100644
--- a/media/jni/android_media_Utils.h
+++ b/media/jni/android_media_Utils.h
@@ -35,6 +35,7 @@
size_t mSize;
public:
+ FileStream(const int fd);
FileStream(const String8 filename);
~FileStream();
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
new file mode 100644
index 0000000..477cd3a
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
new file mode 100644
index 0000000..78ac703
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
new file mode 100644
index 0000000..5fcc720
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/values/exifinterface.xml b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
new file mode 100644
index 0000000..8fc6adc
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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>
+ <array name="exifbyteorderii_jpg">
+ <item>true</item>
+ <item>512</item>
+ <item>288</item>
+ <item>false</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>SAMSUNG</item>
+ <item>SM-N900S</item>
+ <item>2.200</item>
+ <item>2016:01:29 18:32:27</item>
+ <item>0.033</item>
+ <item>0</item>
+ <item>413/100</item>
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item>480</item>
+ <item>640</item>
+ <item>50</item>
+ <item>6</item>
+ <item>0</item>
+ </array>
+ <array name="exifbyteordermm_jpg">
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>true</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>LGE</item>
+ <item>Nexus 5</item>
+ <item>2.400</item>
+ <item>2016:01:29 15:44:58</item>
+ <item>0.017</item>
+ <item>0</item>
+ <item>3970/1000</item>
+ <item>0/1000</item>
+ <item>0</item>
+ <item>1970:01:01</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>N</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>E</item>
+ <item>GPS</item>
+ <item>00:00:00</item>
+ <item>176</item>
+ <item>144</item>
+ <item>146</item>
+ <item>0</item>
+ <item>0</item>
+ </array>
+ <array name="lg_g4_iso_800_dng">
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>true</item>
+ <item>53.834507</item>
+ <item>10.69585</item>
+ <item>0.0</item>
+ <item>LGE</item>
+ <item>LG-H815</item>
+ <item>1.800</item>
+ <item>2015:11:12 16:46:18</item>
+ <item>0.0040</item>
+ <item>0.0</item>
+ <item>442/100</item>
+ <item>0/1</item>
+ <item>0</item>
+ <item>1970:01:17</item>
+ <item>53/1,50/1,423/100</item>
+ <item>N</item>
+ <item>10/1,41/1,4506/100</item>
+ <item>E</item>
+ <item />
+ <item>18:08:10</item>
+ <item>337</item>
+ <item>600</item>
+ <item>800</item>
+ <item>1</item>
+ <item />
+ </array>
+</resources>
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
index 11d9070..61dede3 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
@@ -50,6 +50,7 @@
addMediaScannerUnitTests(suite);
addCameraUnitTests(suite);
addImageReaderTests(suite);
+ addExifInterfaceTests(suite);
return suite;
}
@@ -109,4 +110,8 @@
private void addMediaScannerUnitTests(TestSuite suite) {
suite.addTestSuite(MediaInserterTest.class);
}
+
+ private void addExifInterfaceTests(TestSuite suite) {
+ suite.addTestSuite(ExifInterfaceTest.class);
+ }
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
new file mode 100644
index 0000000..1c80746
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2016 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.mediaframeworktest.unit;
+
+import com.android.mediaframeworktest.R;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+public class ExifInterfaceTest extends AndroidTestCase {
+ private static final String TAG = ExifInterface.class.getSimpleName();
+ private static final boolean VERBOSE = false; // lots of logging
+
+ private static final double DIFFERENCE_TOLERANCE = .005;
+ private static final int BUFFER_SIZE = 32768;
+
+ // List of files.
+ private static final String EXIF_BYTE_ORDER_II_JPEG = "ExifByteOrderII.jpg";
+ private static final String EXIF_BYTE_ORDER_MM_JPEG = "ExifByteOrderMM.jpg";
+ private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
+ private static final int[] IMAGE_RESOURCES = new int[] {
+ R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800 };
+ private static final String[] IMAGE_FILENAMES = new String[] {
+ EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG };
+
+ private static final String[] EXIF_TAGS = {
+ ExifInterface.TAG_MAKE,
+ ExifInterface.TAG_MODEL,
+ ExifInterface.TAG_APERTURE,
+ ExifInterface.TAG_DATETIME,
+ ExifInterface.TAG_EXPOSURE_TIME,
+ ExifInterface.TAG_FLASH,
+ ExifInterface.TAG_FOCAL_LENGTH,
+ ExifInterface.TAG_GPS_ALTITUDE,
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
+ ExifInterface.TAG_GPS_DATESTAMP,
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ ExifInterface.TAG_GPS_TIMESTAMP,
+ ExifInterface.TAG_IMAGE_LENGTH,
+ ExifInterface.TAG_IMAGE_WIDTH,
+ ExifInterface.TAG_ISO,
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.TAG_WHITE_BALANCE
+ };
+
+ private static class ExpectedValue {
+ // Thumbnail information.
+ public final boolean hasThumbnail;
+ public final int thumbnailWidth;
+ public final int thumbnailHeight;
+
+ // GPS information.
+ public final boolean hasLatLong;
+ public final float latitude;
+ public final float longitude;
+ public final float altitude;
+
+ // Values.
+ public final String make;
+ public final String model;
+ public final float aperture;
+ public final String datetime;
+ public final float exposureTime;
+ public final float flash;
+ public final String focalLength;
+ public final String gpsAltitude;
+ public final String gpsAltitudeRef;
+ public final String gpsDatestamp;
+ public final String gpsLatitude;
+ public final String gpsLatitudeRef;
+ public final String gpsLongitude;
+ public final String gpsLongitudeRef;
+ public final String gpsProcessingMethod;
+ public final String gpsTimestamp;
+ public final String imageLength;
+ public final String imageWidth;
+ public final String iso;
+ public final String whiteBalance;
+ public final String orientation;
+
+ private static String getString(TypedArray typedArray, int index) {
+ String stringValue = typedArray.getString(index);
+ if (stringValue == null || stringValue.equals("")) {
+ return null;
+ }
+ return stringValue.trim();
+ }
+
+ public ExpectedValue(TypedArray typedArray) {
+ // Reads thumbnail information.
+ hasThumbnail = typedArray.getBoolean(0, false);
+ thumbnailWidth = typedArray.getInt(1, 0);
+ thumbnailHeight = typedArray.getInt(2, 0);
+
+ // Reads GPS information.
+ hasLatLong = typedArray.getBoolean(3, false);
+ latitude = typedArray.getFloat(4, 0f);
+ longitude = typedArray.getFloat(5, 0f);
+ altitude = typedArray.getFloat(6, 0f);
+
+ // Read values.
+ make = getString(typedArray, 7);
+ model = getString(typedArray, 8);
+ aperture = typedArray.getFloat(9, 0f);
+ datetime = getString(typedArray, 10);
+ exposureTime = typedArray.getFloat(11, 0f);
+ flash = typedArray.getFloat(12, 0f);
+ focalLength = getString(typedArray, 13);
+ gpsAltitude = getString(typedArray, 14);
+ gpsAltitudeRef = getString(typedArray, 15);
+ gpsDatestamp = getString(typedArray, 16);
+ gpsLatitude = getString(typedArray, 17);
+ gpsLatitudeRef = getString(typedArray, 18);
+ gpsLongitude = getString(typedArray, 19);
+ gpsLongitudeRef = getString(typedArray, 20);
+ gpsProcessingMethod = getString(typedArray, 21);
+ gpsTimestamp = getString(typedArray, 22);
+ imageLength = getString(typedArray, 23);
+ imageWidth = getString(typedArray, 24);
+ iso = getString(typedArray, 25);
+ orientation = getString(typedArray, 26);
+ whiteBalance = getString(typedArray, 27);
+
+ typedArray.recycle();
+ }
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String outputPath = new File(Environment.getExternalStorageDirectory(),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ try (InputStream inputStream = getContext().getResources().openRawResource(
+ IMAGE_RESOURCES[i])) {
+ try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
+ Streams.copy(inputStream, outputStream);
+ }
+ }
+ }
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String imageFilePath = new File(Environment.getExternalStorageDirectory(),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ File imageFile = new File(imageFilePath);
+ if (imageFile.exists()) {
+ imageFile.delete();
+ }
+ }
+
+ super.tearDown();
+ }
+
+ private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
+ // Prints thumbnail information.
+ if (exifInterface.hasThumbnail()) {
+ byte[] thumbnailBytes = exifInterface.getThumbnail();
+ if (thumbnailBytes != null) {
+ Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(
+ thumbnailBytes, 0, thumbnailBytes.length);
+ if (bitmap == null) {
+ Log.e(TAG, fileName + " Corrupted thumbnail!");
+ } else {
+ Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", "
+ + bitmap.getHeight());
+ }
+ } else {
+ Log.e(TAG, fileName + " Corrupted image (no thumbnail)");
+ }
+ } else {
+ if (exifInterface.getThumbnail() != null) {
+ Log.e(TAG, fileName + " Corrupted image (a thumbnail exists)");
+ } else {
+ Log.v(TAG, fileName + " No thumbnail");
+ }
+ }
+
+ // Prints GPS information.
+ Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
+
+ float[] latLong = new float[2];
+ if (exifInterface.getLatLong(latLong)) {
+ Log.v(TAG, fileName + " Latitude = " + latLong[0]);
+ Log.v(TAG, fileName + " Longitude = " + latLong[1]);
+ } else {
+ Log.v(TAG, fileName + "No latlong data");
+ }
+
+ // Prints values.
+ for (String tagKey : EXIF_TAGS) {
+ String tagValue = exifInterface.getAttribute(tagKey);
+ Log.v(TAG, fileName + "Key{" + tagKey + "} = '" + tagValue + "'");
+ }
+ }
+
+ private void compareFloatTag(ExifInterface exifInterface, String tag, float expectedValue) {
+ String stringValue = exifInterface.getAttribute(tag);
+ float floatValue = 0f;
+
+ if (stringValue != null) {
+ floatValue = Float.parseFloat(stringValue);
+ }
+
+ assertEquals(expectedValue, floatValue, DIFFERENCE_TOLERANCE);
+ }
+
+ private void compareStringTag(ExifInterface exifInterface, String tag, String expectedValue) {
+ String stringValue = exifInterface.getAttribute(tag);
+ if (stringValue != null) {
+ stringValue = stringValue.trim();
+ }
+
+ assertEquals(expectedValue, stringValue);
+ }
+
+ private void compareWithExpectedValue(ExifInterface exifInterface,
+ ExpectedValue expectedValue) {
+ // Checks a thumbnail image.
+ assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
+ if (expectedValue.hasThumbnail) {
+ byte[] thumbnailBytes = exifInterface.getThumbnail();
+ assertNotNull(thumbnailBytes);
+ Bitmap thumbnailBitmap =
+ BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.length);
+ assertNotNull(thumbnailBitmap);
+ assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
+ assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
+ } else {
+ assertNull(exifInterface.getThumbnail());
+ }
+
+ // Checks GPS information.
+ float[] latLong = new float[2];
+ assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+ if (expectedValue.hasLatLong) {
+ assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
+ assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
+ }
+ assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
+
+ // Checks values.
+ compareStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make);
+ compareStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model);
+ compareFloatTag(exifInterface, ExifInterface.TAG_APERTURE, expectedValue.aperture);
+ compareStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime);
+ compareFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime);
+ compareFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash);
+ compareStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF,
+ expectedValue.gpsAltitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP,
+ expectedValue.gpsDatestamp);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF,
+ expectedValue.gpsLatitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE,
+ expectedValue.gpsLongitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF,
+ expectedValue.gpsLongitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ expectedValue.gpsProcessingMethod);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP,
+ expectedValue.gpsTimestamp);
+ compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength);
+ compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth);
+ compareStringTag(exifInterface, ExifInterface.TAG_ISO, expectedValue.iso);
+ compareStringTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation);
+ compareStringTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE,
+ expectedValue.whiteBalance);
+ }
+
+ private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+ // Created via path.
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+
+ // Created via InputStream.
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(imageFile.getAbsolutePath());
+ exifInterface = new ExifInterface(in);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+
+ // Created via FileDescriptor.
+ try {
+ FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600);
+ exifInterface = new ExifInterface(fd);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+
+ // Test for saving attributes.
+ try {
+ FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
+ exifInterface = new ExifInterface(fd);
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(fd);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+
+ // Test for modifying one attribute.
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
+ }
+
+ private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+ // Created via path.
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+
+ // Created via FileDescriptor.
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(imageFile);
+ exifInterface = new ExifInterface(in.getFD());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
+ }
+
+ public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
+ }
+
+ public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
+ testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
+ }
+}
diff --git a/packages/SystemUI/res/layout/recents_task_view_header.xml b/packages/SystemUI/res/layout/recents_task_view_header.xml
index deb8e91..2e41561 100644
--- a/packages/SystemUI/res/layout/recents_task_view_header.xml
+++ b/packages/SystemUI/res/layout/recents_task_view_header.xml
@@ -26,13 +26,16 @@
android:layout_width="@dimen/recents_task_view_header_icon_width"
android:layout_height="@dimen/recents_task_view_header_icon_height"
android:layout_gravity="center_vertical|start"
- android:padding="9dp" />
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="16dp" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
- android:layout_marginStart="64dp"
+ android:layout_marginStart="56dp"
android:layout_marginEnd="112dp"
android:textSize="16sp"
android:textColor="#ffffffff"
@@ -48,7 +51,7 @@
android:layout_height="@dimen/recents_task_view_header_button_height"
android:layout_marginEnd="@dimen/recents_task_view_header_button_width"
android:layout_gravity="center_vertical|end"
- android:padding="15dp"
+ android:padding="13dp"
android:src="@drawable/star"
android:background="?android:selectableItemBackground"
android:alpha="0"
@@ -58,7 +61,7 @@
android:layout_width="@dimen/recents_task_view_header_button_width"
android:layout_height="@dimen/recents_task_view_header_button_height"
android:layout_gravity="center_vertical|end"
- android:padding="15dp"
+ android:padding="13dp"
android:src="@drawable/recents_dismiss_light"
android:background="?android:selectableItemBackground"
android:alpha="0"
diff --git a/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
index 10659a3..1becdab 100644
--- a/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
+++ b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
@@ -23,13 +23,16 @@
android:layout_width="@dimen/recents_task_view_header_icon_width"
android:layout_height="@dimen/recents_task_view_header_icon_height"
android:layout_gravity="center_vertical|start"
- android:padding="9dp" />
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="16dp" />
<TextView
android:id="@+id/app_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
- android:layout_marginStart="64dp"
+ android:layout_marginStart="56dp"
android:layout_marginEnd="112dp"
android:textSize="16sp"
android:textColor="#ffffffff"
@@ -44,7 +47,7 @@
android:layout_width="@dimen/recents_task_view_header_button_width"
android:layout_height="@dimen/recents_task_bar_height"
android:layout_gravity="center_vertical|end"
- android:padding="15dp"
+ android:padding="13dp"
android:background="?android:selectableItemBackground"
android:src="@drawable/recents_info_light" />
</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 177102f..fbe0207 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -235,7 +235,7 @@
<dimen name="glowpadview_inner_radius">15dip</dimen>
<!-- The size of the icon in the recents task view header. -->
- <dimen name="recents_task_view_header_icon_width">64dp</dimen>
+ <dimen name="recents_task_view_header_icon_width">56dp</dimen>
<dimen name="recents_task_view_header_icon_height">@dimen/recents_task_bar_height</dimen>
<!-- The size of a button in the recents task view header. -->
@@ -263,7 +263,7 @@
<dimen name="recents_task_view_affiliate_group_enter_offset">32dp</dimen>
<!-- The height of a task view bar. -->
- <dimen name="recents_task_bar_height">56dp</dimen>
+ <dimen name="recents_task_bar_height">50dp</dimen>
<!-- The height of the search bar space. -->
<dimen name="recents_search_bar_space_height">64dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 26c3f3a..6f6e515 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -186,7 +186,9 @@
<!-- Notification title displayed when we fail to take a screenshot. [CHAR LIMIT=50] -->
<string name="screenshot_failed_title">Couldn\'t capture screenshot.</string>
<!-- Notification text displayed when we fail to take a screenshot. [CHAR LIMIT=100] -->
- <string name="screenshot_failed_text">Can\'t take screenshot due to limited storage space, or it isn\'t allowed by the app or your organization.</string>
+ <string name="screenshot_failed_to_save_text">Can\'t save screenshot due to limited storage space.</string>
+ <!-- Notification text displayed when we fail to take a screenshot. [CHAR LIMIT=100] -->
+ <string name="screenshot_failed_to_capture_text">Taking screenshots is not allowed by the app or your organization.</string>
<!-- Title for the USB function chooser in UsbPreferenceActivity. [CHAR LIMIT=30] -->
<string name="usb_preference_title">USB file transfer options</string>
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
index e86b92d..72b1cab 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
@@ -196,7 +196,7 @@
* are not called.
*/
public static void cancelAnimationWithoutCallbacks(Animator animator) {
- if (animator != null) {
+ if (animator != null && animator.isStarted()) {
removeAnimationListenersRecursive(animator);
animator.cancel();
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 88bebdb..261b6f6 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -16,20 +16,15 @@
package com.android.systemui.recents.views;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Path;
import android.graphics.Rect;
-import android.util.ArrayMap;
import android.util.ArraySet;
-import android.util.FloatProperty;
-import android.util.Property;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
import android.view.ViewDebug;
-import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsActivityLaunchState;
@@ -105,41 +100,20 @@
*/
public class TaskStackLayoutAlgorithm {
- // The scale factor to apply to the user movement in the stack to unfocus it
- private static final float UNFOCUS_MULTIPLIER = 0.8f;
-
// The distribution of view bounds alpha
// XXX: This is a hack because you can currently set the max alpha to be > 1f
public static final float OUTLINE_ALPHA_MIN_VALUE = 0f;
public static final float OUTLINE_ALPHA_MAX_VALUE = 2f;
- // The distribution of dim to apply to tasks in the stack
- private static final float DIM_MAX_VALUE = 0.35f;
- private static final Path UNFOCUSED_DIM_PATH = new Path();
- private static final Path FOCUSED_DIM_PATH = new Path();
- static {
- // The unfocused dim interpolator peaks to 1 at 0.5 (the focused task), then slowly drops
- // back to 0.5 at the front of the stack
- UNFOCUSED_DIM_PATH.moveTo(0f, 0f);
- UNFOCUSED_DIM_PATH.cubicTo(0f, 0.1f, 0.4f, 0.8f, 0.5f, 1f);
- UNFOCUSED_DIM_PATH.cubicTo(0.6f, 1f, 0.9f, 0.6f, 1f, 0.5f);
- // The focused dim interpolator peaks to 1 at 0.5 (the focused task), then drops back to 0
- // at the front of the stack
- FOCUSED_DIM_PATH.moveTo(0f, 0f);
- FOCUSED_DIM_PATH.cubicTo(0.1f, 0f, 0.4f, 1f, 0.5f, 1f);
- FOCUSED_DIM_PATH.cubicTo(0.6f, 1f, 0.9f, 0f, 1f, 0f);
- }
- private static final FreePathInterpolator UNFOCUSED_DIM_INTERPOLATOR =
- new FreePathInterpolator(UNFOCUSED_DIM_PATH);
- private static final FreePathInterpolator FOCUSED_DIM_INTERPOLATOR =
- new FreePathInterpolator(FOCUSED_DIM_PATH);
+ // The maximum dim on the tasks
+ private static final float MAX_DIM = 0.25f;
// The various focus states
- public static final float STATE_FOCUSED = 1f;
- public static final float STATE_UNFOCUSED = 0f;
+ public static final int STATE_FOCUSED = 1;
+ public static final int STATE_UNFOCUSED = 0;
public interface TaskStackLayoutAlgorithmCallbacks {
- void onFocusStateChanged(float prevFocusState, float curFocusState);
+ void onFocusStateChanged(int prevFocusState, int curFocusState);
}
/**
@@ -209,24 +183,6 @@
}
}
- /**
- * A Property wrapper around the <code>focusState</code> functionality handled by the
- * {@link TaskStackLayoutAlgorithm#setFocusState(float)} and
- * {@link TaskStackLayoutAlgorithm#getFocusState()} methods.
- */
- private static final Property<TaskStackLayoutAlgorithm, Float> FOCUS_STATE =
- new FloatProperty<TaskStackLayoutAlgorithm>("focusState") {
- @Override
- public void setValue(TaskStackLayoutAlgorithm object, float value) {
- object.setFocusState(value);
- }
-
- @Override
- public Float get(TaskStackLayoutAlgorithm object) {
- return object.getFocusState();
- }
- };
-
// A report of the visibility state of the stack
public class VisibilityReport {
public int numVisibleTasks;
@@ -286,13 +242,17 @@
private FreePathInterpolator mUnfocusedCurveInterpolator;
private FreePathInterpolator mFocusedCurveInterpolator;
+ // The paths defining the distribution of the dim to apply to tasks in the stack when focused
+ // and unfocused
+ private Path mUnfocusedDimCurve;
+ private Path mFocusedDimCurve;
+ private FreePathInterpolator mUnfocusedDimCurveInterpolator;
+ private FreePathInterpolator mFocusedDimCurveInterpolator;
+
// The state of the stack focus (0..1), which controls the transition of the stack from the
// focused to non-focused state
@ViewDebug.ExportedProperty(category="recents")
- private float mFocusState;
-
- // The animator used to reset the focused state
- private ObjectAnimator mFocusStateAnimator;
+ private int mFocusState;
// The smallest scroll progress, at this value, the back most task will be visible
@ViewDebug.ExportedProperty(category="recents")
@@ -321,7 +281,8 @@
int mMaxTranslationZ;
// Optimization, allows for quick lookup of task -> index
- private ArrayMap<Task.TaskKey, Integer> mTaskIndexMap = new ArrayMap<>();
+ private SparseIntArray mTaskIndexMap = new SparseIntArray();
+ private SparseArray<Float> mTaskIndexOverrideMap = new SparseArray<>();
// The freeform workspace layout
FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
@@ -354,6 +315,7 @@
* Resets this layout when the stack view is reset.
*/
public void reset() {
+ mTaskIndexOverrideMap.clear();
setFocusState(getDefaultFocusState());
}
@@ -367,8 +329,8 @@
/**
* Sets the focused state.
*/
- public void setFocusState(float focusState) {
- float prevFocusState = mFocusState;
+ public void setFocusState(int focusState) {
+ int prevFocusState = mFocusState;
mFocusState = focusState;
updateFrontBackTransforms();
if (mCb != null) {
@@ -379,7 +341,7 @@
/**
* Gets the focused state.
*/
- public float getFocusState() {
+ public int getFocusState() {
return mFocusState;
}
@@ -424,6 +386,11 @@
mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve);
mFocusedCurve = constructFocusedCurve();
mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve);
+ mUnfocusedDimCurve = constructUnfocusedDimCurve();
+ mUnfocusedDimCurveInterpolator = new FreePathInterpolator(mUnfocusedDimCurve);
+ mFocusedDimCurve = constructFocusedDimCurve();
+ mFocusedDimCurveInterpolator = new FreePathInterpolator(mFocusedDimCurve);
+
updateFrontBackTransforms();
}
@@ -469,7 +436,7 @@
int taskCount = stackTasks.size();
for (int i = 0; i < taskCount; i++) {
Task task = stackTasks.get(i);
- mTaskIndexMap.put(task.key, i);
+ mTaskIndexMap.put(task.key.id, i);
}
// Calculate the min/max scroll
@@ -516,35 +483,56 @@
}
/**
- * Updates this stack when a scroll happens.
+ * Adds and override task progress for the given task when transitioning from focused to
+ * unfocused state.
*/
- public void updateFocusStateOnScroll(int yMovement) {
- Utilities.cancelAnimationWithoutCallbacks(mFocusStateAnimator);
- if (mFocusState > STATE_UNFOCUSED) {
- float delta = (float) yMovement / (UNFOCUS_MULTIPLIER * mStackRect.height());
- setFocusState(mFocusState - Math.min(mFocusState, Math.abs(delta)));
+ public void addUnfocusedTaskOverride(Task task, float stackScroll) {
+ if (mFocusState != STATE_UNFOCUSED) {
+ mFocusedRange.offset(stackScroll);
+ mUnfocusedRange.offset(stackScroll);
+ float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id));
+ float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX);
+ float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY);
+ float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX);
+ if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) {
+ mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress);
+ }
}
}
/**
- * Aniamtes the focused state back to its orginal state.
+ * Updates this stack when a scroll happens.
*/
- public void animateFocusState(float newState) {
- Utilities.cancelAnimationWithoutCallbacks(mFocusStateAnimator);
- if (Float.compare(newState, getFocusState()) != 0) {
- mFocusStateAnimator = ObjectAnimator.ofFloat(this, FOCUS_STATE, getFocusState(),
- newState);
- mFocusStateAnimator.setDuration(mContext.getResources().getInteger(
- R.integer.recents_animate_task_stack_scroll_duration));
- mFocusStateAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
- mFocusStateAnimator.start();
+ public void updateFocusStateOnScroll(float stackScroll, float deltaScroll) {
+ for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) {
+ int taskId = mTaskIndexOverrideMap.keyAt(i);
+ float x = mTaskIndexMap.get(taskId);
+ float overrideX = mTaskIndexOverrideMap.get(taskId, 0f);
+ float newOverrideX = overrideX + deltaScroll;
+ mUnfocusedRange.offset(stackScroll);
+ boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f ||
+ mUnfocusedRange.getNormalizedX(newOverrideX) > 1f;
+ if (outOfBounds || (overrideX >= x && x >= newOverrideX) ||
+ (overrideX <= x && x <= newOverrideX)) {
+ // Remove the override once we reach the original task index
+ mTaskIndexOverrideMap.removeAt(i);
+ } else if ((overrideX >= x && deltaScroll <= 0f) ||
+ (overrideX <= x && deltaScroll >= 0f)) {
+ // Scrolling from override x towards x, then lock the task in place
+ mTaskIndexOverrideMap.put(taskId, newOverrideX);
+ } else {
+ // Scrolling override x away from x, we should still move the scroll towards x
+ float deltaX = overrideX - x;
+ newOverrideX = Math.signum(deltaX) * (Math.abs(deltaX) - deltaScroll);
+ mTaskIndexOverrideMap.put(taskId, x + newOverrideX);
+ }
}
}
/**
* Returns the default focus state.
*/
- public float getDefaultFocusState() {
+ public int getDefaultFocusState() {
return STATE_FOCUSED;
}
@@ -650,18 +638,19 @@
false /* forceUpdate */);
}
- public TaskViewTransform getStackTransform(Task task, float stackScroll, float focusState,
+ public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState,
TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate) {
if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
return transformOut;
} else {
// Return early if we have an invalid index
- if (task == null || !mTaskIndexMap.containsKey(task.key)) {
+ if (task == null || mTaskIndexMap.get(task.key.id, -1) == -1) {
transformOut.reset();
return transformOut;
}
- getStackTransform(mTaskIndexMap.get(task.key), stackScroll, focusState, transformOut,
+ float taskProgress = getStackScrollForTask(task);
+ getStackTransform(taskProgress, stackScroll, focusState, transformOut,
frontTransform, false /* ignoreSingleTaskCase */, forceUpdate);
return transformOut;
}
@@ -687,7 +676,7 @@
* internally to ensure that we can calculate the transform for any
* position in the stack.
*/
- public void getStackTransform(float taskProgress, float stackScroll, float focusState,
+ public void getStackTransform(float taskProgress, float stackScroll, int focusState,
TaskViewTransform transformOut, TaskViewTransform frontTransform,
boolean ignoreSingleTaskCase, boolean forceUpdate) {
SystemServicesProxy ssp = Recents.getSystemServices();
@@ -734,16 +723,16 @@
unfocusedRangeX)) * mStackRect.height());
int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation(
focusedRangeX)) * mStackRect.height());
- float unfocusedDim = 1f - UNFOCUSED_DIM_INTERPOLATOR.getInterpolation(
+ float unfocusedDim = mUnfocusedDimCurveInterpolator.getInterpolation(
boundedScrollUnfocusedRangeX);
- float focusedDim = 1f - FOCUSED_DIM_INTERPOLATOR.getInterpolation(
+ float focusedDim = mFocusedDimCurveInterpolator.getInterpolation(
boundedScrollFocusedRangeX);
y = (mStackRect.top - mTaskRect.top) +
(int) Utilities.mapRange(focusState, unfocusedY, focusedY);
z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX),
mMinTranslationZ, mMaxTranslationZ);
- dimAlpha = DIM_MAX_VALUE * Utilities.mapRange(focusState, unfocusedDim, focusedDim);
+ dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim);
viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX),
OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE);
}
@@ -773,8 +762,7 @@
* stack.
*/
float getStackScrollForTask(Task t) {
- if (!mTaskIndexMap.containsKey(t.key)) return 0f;
- return mTaskIndexMap.get(t.key);
+ return mTaskIndexOverrideMap.get(t.key.id, (float) mTaskIndexMap.get(t.key.id, 0));
}
/**
@@ -842,6 +830,34 @@
}
/**
+ * Creates a new path for the focused dim curve.
+ */
+ private Path constructFocusedDimCurve() {
+ Path p = new Path();
+ // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused
+ // task), then goes back to max dim at the next task
+ p.moveTo(0f, MAX_DIM);
+ p.lineTo(0.5f, 0f);
+ p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM);
+ p.lineTo(1f, MAX_DIM);
+ return p;
+ }
+
+ /**
+ * Creates a new path for the unfocused dim curve.
+ */
+ private Path constructUnfocusedDimCurve() {
+ Path p = new Path();
+ // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused
+ // task), then goes back to max dim towards the front of the stack
+ p.moveTo(0f, MAX_DIM);
+ p.cubicTo(0f, 0.1f, 0.4f, 0f, 0.5f, 0f);
+ p.cubicTo(0.6f, 0f, 0.9f, MAX_DIM - 0.1f, 1f, MAX_DIM / 2f);
+ return p;
+ }
+
+
+ /**
* Updates the current transforms that would put a TaskView at the front and back of the stack.
*/
private void updateFrontBackTransforms() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 1707c4f..d71d70f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -35,6 +35,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.MutableBoolean;
+import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -163,12 +164,11 @@
@ViewDebug.ExportedProperty(category="recents")
private Rect mStackBounds = new Rect();
- @ViewDebug.ExportedProperty(category="recents")
- private int[] mTmpVisibleRange = new int[2];
private Rect mTmpRect = new Rect();
private ArrayMap<Task.TaskKey, TaskView> mTmpTaskViewMap = new ArrayMap<>();
private List<TaskView> mTmpTaskViews = new ArrayList<>();
private TaskViewTransform mTmpTransform = new TaskViewTransform();
+ private int[] mTmpIntPair = new int[2];
// A convenience update listener to request updating clipping of tasks
private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener =
@@ -405,15 +405,17 @@
* target stack scrolls will be returned.
* @param ignoreTasksSet The set of tasks to skip for purposes of calculaing the visible range.
* Transforms will still be calculated for the ignore tasks.
+ * @return the front and back most visible task indices (there may be non visible tasks in
+ * between this range)
*/
- boolean computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms,
+ int[] computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms,
ArrayList<Task> tasks, float curStackScroll, float targetStackScroll,
- int[] visibleRangeOut, ArraySet<Task.TaskKey> ignoreTasksSet) {
+ ArraySet<Task.TaskKey> ignoreTasksSet) {
int taskCount = tasks.size();
- int frontMostVisibleIndex = -1;
- int backMostVisibleIndex = -1;
+ int[] visibleTaskRange = mTmpIntPair;
+ visibleTaskRange[0] = -1;
+ visibleTaskRange[1] = -1;
boolean useTargetStackScroll = Float.compare(curStackScroll, targetStackScroll) != 0;
- boolean targetScrollIsInFront = targetStackScroll > curStackScroll;
// We can reuse the task transforms where possible to reduce object allocation
Utilities.matchTaskListSize(tasks, taskTransforms);
@@ -452,31 +454,16 @@
continue;
}
- if (transform.visible) {
- if (frontMostVisibleIndex < 0) {
- frontMostVisibleIndex = i;
- }
- backMostVisibleIndex = i;
- } else if (!targetScrollIsInFront) {
- if (backMostVisibleIndex != -1) {
- // We've reached the end of the visible range, so going down the rest of the
- // stack, we can just reset the transforms accordingly
- while (i >= 0) {
- taskTransforms.get(i).reset();
- i--;
- }
- break;
- }
- }
-
frontTransform = transform;
frontTransformAtTarget = transformAtTarget;
+ if (transform.visible) {
+ if (visibleTaskRange[0] < 0) {
+ visibleTaskRange[0] = i;
+ }
+ visibleTaskRange[1] = i;
+ }
}
- if (visibleRangeOut != null) {
- visibleRangeOut[0] = frontMostVisibleIndex;
- visibleRangeOut[1] = backMostVisibleIndex;
- }
- return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1;
+ return visibleTaskRange;
}
/**
@@ -502,13 +489,10 @@
* {@link TaskView}s
*/
void bindVisibleTaskViews(float targetStackScroll, ArraySet<Task.TaskKey> ignoreTasksSet) {
- final int[] visibleStackRange = mTmpVisibleRange;
-
// Get all the task transforms
- final ArrayList<Task> tasks = mStack.getStackTasks();
- final boolean isValidVisibleRange = computeVisibleTaskTransforms(mCurrentTaskTransforms,
- tasks, mStackScroller.getStackScroll(), targetStackScroll, visibleStackRange,
- ignoreTasksSet);
+ ArrayList<Task> tasks = mStack.getStackTasks();
+ int[] visibleTaskRange = computeVisibleTaskTransforms(mCurrentTaskTransforms, tasks,
+ mStackScroller.getStackScroll(), targetStackScroll, ignoreTasksSet);
// Return all the invisible children to the pool
mTmpTaskViewMap.clear();
@@ -519,14 +503,14 @@
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
int taskIndex = mStack.indexOfStackTask(task);
+ TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex);
// Skip ignored tasks
if (ignoreTasksSet.contains(task.key)) {
continue;
}
- if (task.isFreeformTask() ||
- visibleStackRange[1] <= taskIndex && taskIndex <= visibleStackRange[0]) {
+ if (task.isFreeformTask() || transform.visible) {
mTmpTaskViewMap.put(task.key, tv);
} else {
if (mTouchExplorationEnabled) {
@@ -538,8 +522,7 @@
}
// Pick up all the newly visible children
- int lastVisStackIndex = isValidVisibleRange ? visibleStackRange[1] : 0;
- for (int i = tasks.size() - 1; i >= lastVisStackIndex; i--) {
+ for (int i = tasks.size() - 1; i >= 0; i--) {
Task task = tasks.get(i);
TaskViewTransform transform = mCurrentTaskTransforms.get(i);
@@ -584,11 +567,11 @@
// Update the focus if the previous focused task was returned to the view pool
if (lastFocusedTaskIndex != -1) {
- if (lastFocusedTaskIndex < visibleStackRange[1]) {
- setFocusedTask(visibleStackRange[1], false /* scrollToTask */,
+ if (lastFocusedTaskIndex < visibleTaskRange[1]) {
+ setFocusedTask(visibleTaskRange[1], false /* scrollToTask */,
true /* requestViewFocus */);
} else {
- setFocusedTask(visibleStackRange[0], false /* scrollToTask */,
+ setFocusedTask(visibleTaskRange[0], false /* scrollToTask */,
true /* requestViewFocus */);
}
}
@@ -665,7 +648,7 @@
public void getCurrentTaskTransforms(ArrayList<Task> tasks,
ArrayList<TaskViewTransform> transformsOut) {
Utilities.matchTaskListSize(tasks, transformsOut);
- float focusState = mLayoutAlgorithm.getFocusState();
+ int focusState = mLayoutAlgorithm.getFocusState();
for (int i = tasks.size() - 1; i >= 0; i--) {
Task task = tasks.get(i);
TaskViewTransform transform = transformsOut.get(i);
@@ -684,7 +667,7 @@
* Returns the task transforms for all the tasks in the stack if the stack was at the given
* {@param stackScroll} and {@param focusState}.
*/
- public void getLayoutTaskTransforms(float stackScroll, float focusState, ArrayList<Task> tasks,
+ public void getLayoutTaskTransforms(float stackScroll, int focusState, ArrayList<Task> tasks,
ArrayList<TaskViewTransform> transformsOut) {
Utilities.matchTaskListSize(tasks, transformsOut);
for (int i = tasks.size() - 1; i >= 0; i--) {
@@ -1055,7 +1038,7 @@
protected Parcelable onSaveInstanceState() {
Bundle savedState = new Bundle();
savedState.putParcelable(KEY_SAVED_STATE_SUPER, super.onSaveInstanceState());
- savedState.putFloat(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE, mLayoutAlgorithm.getFocusState());
+ savedState.putInt(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE, mLayoutAlgorithm.getFocusState());
savedState.putFloat(KEY_SAVED_STATE_LAYOUT_STACK_SCROLL, mStackScroller.getStackScroll());
return super.onSaveInstanceState();
}
@@ -1065,7 +1048,7 @@
Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(KEY_SAVED_STATE_SUPER));
- mLayoutAlgorithm.setFocusState(savedState.getFloat(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE));
+ mLayoutAlgorithm.setFocusState(savedState.getInt(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE));
mStackScroller.setStackScroll(savedState.getFloat(KEY_SAVED_STATE_LAYOUT_STACK_SCROLL));
}
@@ -1517,7 +1500,7 @@
/**** TaskStackLayoutAlgorithm.TaskStackLayoutAlgorithmCallbacks ****/
@Override
- public void onFocusStateChanged(float prevFocusState, float curFocusState) {
+ public void onFocusStateChanged(int prevFocusState, int curFocusState) {
if (mDeferredTaskViewLayoutAnimation == null) {
mUIDozeTrigger.poke();
relayoutTaskViewsOnNextFrame(AnimationProps.IMMEDIATE);
@@ -1532,6 +1515,7 @@
if (animation != null) {
relayoutTaskViewsOnNextFrame(animation);
}
+ mLayoutAlgorithm.updateFocusStateOnScroll(curScroll, curScroll - prevScroll);
if (mEnterAnimationComplete) {
if (shouldShowHistoryButton() &&
@@ -1636,11 +1620,19 @@
}
public final void onBusEvent(FocusNextTaskViewEvent event) {
+ // Stop any scrolling
+ mStackScroller.stopScroller();
+ mStackScroller.stopBoundScrollAnimation();
+
setRelativeFocusedTask(true, false /* stackTasksOnly */, true /* animated */, false,
event.timerIndicatorDuration);
}
public final void onBusEvent(FocusPreviousTaskViewEvent event) {
+ // Stop any scrolling
+ mStackScroller.stopScroller();
+ mStackScroller.stopBoundScrollAnimation();
+
setRelativeFocusedTask(false, false /* stackTasksOnly */, true /* animated */);
}
@@ -1771,10 +1763,6 @@
removeIgnoreTask(event.task);
}
- public final void onBusEvent(StackViewScrolledEvent event) {
- mLayoutAlgorithm.updateFocusStateOnScroll(event.yMovement.value);
- }
-
public final void onBusEvent(IterateRecentsEvent event) {
if (!mEnterAnimationComplete) {
// Cancel the previous task's window transition before animating the focused state
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index 3f0630d..20933ee 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -223,6 +223,13 @@
int xDiff = Math.abs(x - mDownX);
if (Math.abs(y - mDownY) > mScrollTouchSlop && yDiff > xDiff) {
mIsScrolling = true;
+ float stackScroll = mScroller.getStackScroll();
+ List<TaskView> taskViews = mSv.getTaskViews();
+ for (int i = taskViews.size() - 1; i >= 0; i--) {
+ layoutAlgorithm.addUnfocusedTaskOverride(taskViews.get(i).getTask(),
+ stackScroll);
+ }
+ layoutAlgorithm.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED);
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
@@ -429,8 +436,7 @@
// Otherwise, offset the scroll by the movement of the anchor task
float anchorTaskScroll = layoutAlgorithm.getStackScrollForTask(anchorTask);
float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
- if (layoutAlgorithm.getFocusState() !=
- TaskStackLayoutAlgorithm.STATE_FOCUSED) {
+ if (layoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) {
// If we are focused, we don't want the front task to move, but otherwise, we
// allow the back task to move up, and the front task to move back
stackScrollOffset /= 2;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index bb56a52..0543dd5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -63,7 +63,7 @@
public class TaskViewHeader extends FrameLayout
implements View.OnClickListener, View.OnLongClickListener {
- private static final float HIGHLIGHT_LIGHTNESS_INCREMENT = 0.125f;
+ private static final float HIGHLIGHT_LIGHTNESS_INCREMENT = 0.075f;
private static final float OVERLAY_LIGHTNESS_INCREMENT = -0.0625f;
private static final int OVERLAY_REVEAL_DURATION = 250;
private static final long FOCUS_INDICATOR_INTERVAL_MS = 30;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
index 0fec9c3..e46708e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
@@ -183,9 +183,9 @@
mat[0] = scale;
mat[6] = scale;
mat[12] = scale;
- mat[4] = mDimAlpha;
- mat[9] = mDimAlpha;
- mat[14] = mDimAlpha;
+ mat[4] = mDimAlpha * 255f;
+ mat[9] = mDimAlpha * 255f;
+ mat[14] = mDimAlpha * 255f;
TMP_FILTER_COLOR_MATRIX.preConcat(TMP_BRIGHTNESS_COLOR_MATRIX);
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(TMP_FILTER_COLOR_MATRIX);
mDrawPaint.setColorFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index e6a291c..a06700d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -75,9 +75,9 @@
Uri imageUri;
Runnable finisher;
int iconSize;
- int result;
int previewWidth;
int previewheight;
+ int errorMsgResId;
void clearImage() {
image = null;
@@ -94,13 +94,11 @@
*/
class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void,
SaveImageInBackgroundData> {
- private static final String TAG = "SaveImageInBackgroundTask";
private static final String SCREENSHOTS_DIR_NAME = "Screenshots";
private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
- private final int mNotificationId;
private final NotificationManager mNotificationManager;
private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder;
private final File mScreenshotDir;
@@ -119,7 +117,7 @@
private static boolean mTickerAddSpace;
SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
- NotificationManager nManager, int nId) {
+ NotificationManager nManager) {
Resources r = context.getResources();
// Prepare all the output metadata
@@ -166,25 +164,14 @@
// Show the intermediate notification
mTickerAddSpace = !mTickerAddSpace;
- mNotificationId = nId;
mNotificationManager = nManager;
final long now = System.currentTimeMillis();
- mNotificationBuilder = new Notification.Builder(context)
- .setTicker(r.getString(R.string.screenshot_saving_ticker)
- + (mTickerAddSpace ? " " : ""))
- .setContentTitle(r.getString(R.string.screenshot_saving_title))
- .setContentText(r.getString(R.string.screenshot_saving_text))
- .setSmallIcon(R.drawable.stat_notify_image)
- .setWhen(now)
- .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color));
-
+ // Setup the notification
mNotificationStyle = new Notification.BigPictureStyle()
- .bigPicture(picture.createAshmemBitmap());
- mNotificationBuilder.setStyle(mNotificationStyle);
+ .bigPicture(picture.createAshmemBitmap());
- // For "public" situations we want to show all the same info but
- // omit the actual screenshot image.
+ // The public notification will show similar info but with the actual screenshot omitted
mPublicNotificationBuilder = new Notification.Builder(context)
.setContentTitle(r.getString(R.string.screenshot_saving_title))
.setContentText(r.getString(R.string.screenshot_saving_text))
@@ -194,11 +181,24 @@
.setColor(r.getColor(
com.android.internal.R.color.system_notification_accent_color));
- mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build());
+ mNotificationBuilder = new Notification.Builder(context)
+ .setTicker(r.getString(R.string.screenshot_saving_ticker)
+ + (mTickerAddSpace ? " " : ""))
+ .setContentTitle(r.getString(R.string.screenshot_saving_title))
+ .setContentText(r.getString(R.string.screenshot_saving_text))
+ .setSmallIcon(R.drawable.stat_notify_image)
+ .setWhen(now)
+ .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color))
+ .setStyle(mNotificationStyle)
+ .setPublicVersion(mPublicNotificationBuilder.build());
+ mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
- Notification n = mNotificationBuilder.build();
- n.flags |= Notification.FLAG_NO_CLEAR;
- mNotificationManager.notify(nId, n);
+ mNotificationManager.notify(R.id.notification_screenshot, mNotificationBuilder.build());
+
+ /**
+ * NOTE: The following code prepares the notification builder for updating the notification
+ * after the screenshot has been written to disk.
+ */
// On the tablet, the large icon makes the notification appear as if it is clickable (and
// on small devices, the large icon is not shown) so defer showing the large icon until
@@ -210,11 +210,8 @@
@Override
protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) {
- if (params.length != 1) return null;
if (isCancelled()) {
- params[0].clearImage();
- params[0].clearContext();
- return null;
+ return params[0];
}
// By default, AsyncTask sets the worker thread to have background thread priority, so bump
@@ -263,36 +260,37 @@
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
// Create a share action for the notification
- final PendingIntent callback = PendingIntent.getBroadcast(context, 0,
- new Intent(context, GlobalScreenshot.TargetChosenReceiver.class)
- .putExtra(GlobalScreenshot.CANCEL_ID, mNotificationId),
+ PendingIntent chooseAction = PendingIntent.getBroadcast(context, 0,
+ new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Intent chooserIntent = Intent.createChooser(sharingIntent, null,
- callback.getIntentSender());
- chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
- | Intent.FLAG_ACTIVITY_NEW_TASK);
- mNotificationBuilder.addAction(R.drawable.ic_screenshot_share,
- r.getString(com.android.internal.R.string.share),
- PendingIntent.getActivity(context, 0, chooserIntent,
- PendingIntent.FLAG_CANCEL_CURRENT));
+ chooseAction.getIntentSender())
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent shareAction = PendingIntent.getActivity(context, 0, chooserIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_share,
+ r.getString(com.android.internal.R.string.share), shareAction);
+ mNotificationBuilder.addAction(shareActionBuilder.build());
// Create a delete action for the notification
- final PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
+ PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
- .putExtra(GlobalScreenshot.CANCEL_ID, mNotificationId)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
- mNotificationBuilder.addAction(R.drawable.ic_screenshot_delete,
+ Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_delete,
r.getString(com.android.internal.R.string.delete), deleteAction);
+ mNotificationBuilder.addAction(deleteActionBuilder.build());
params[0].imageUri = uri;
params[0].image = null;
- params[0].result = 0;
+ params[0].errorMsgResId = 0;
} catch (Exception e) {
// IOException/UnsupportedOperationException may be thrown if external storage is not
// mounted
params[0].clearImage();
- params[0].result = 1;
+ params[0].errorMsgResId = R.string.screenshot_failed_to_save_text;
}
// Recycle the bitmap data
@@ -305,19 +303,14 @@
@Override
protected void onPostExecute(SaveImageInBackgroundData params) {
- if (isCancelled()) {
- params.finisher.run();
- params.clearImage();
- params.clearContext();
- return;
- }
-
- if (params.result > 0) {
+ if (params.errorMsgResId != 0) {
// Show a message that we've failed to save the image to disk
- GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager);
+ GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager,
+ params.errorMsgResId);
} else {
// Show the final notification to indicate screenshot saved
- Resources r = params.context.getResources();
+ Context context = params.context;
+ Resources r = context.getResources();
// Create the intent to show the screenshot in gallery
Intent launchIntent = new Intent(Intent.ACTION_VIEW);
@@ -326,34 +319,41 @@
final long now = System.currentTimeMillis();
+ // Update the text and the icon for the existing notification
+ mPublicNotificationBuilder
+ .setContentTitle(r.getString(R.string.screenshot_saved_title))
+ .setContentText(r.getString(R.string.screenshot_saved_text))
+ .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0))
+ .setWhen(now)
+ .setAutoCancel(true)
+ .setColor(context.getColor(
+ com.android.internal.R.color.system_notification_accent_color));
mNotificationBuilder
.setContentTitle(r.getString(R.string.screenshot_saved_title))
.setContentText(r.getString(R.string.screenshot_saved_text))
.setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0))
.setWhen(now)
.setAutoCancel(true)
- .setColor(r.getColor(
- com.android.internal.R.color.system_notification_accent_color));;
+ .setColor(context.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setPublicVersion(mPublicNotificationBuilder.build())
+ .setFlag(Notification.FLAG_NO_CLEAR, false);
- // Update the text in the public version as well
- mPublicNotificationBuilder
- .setContentTitle(r.getString(R.string.screenshot_saved_title))
- .setContentText(r.getString(R.string.screenshot_saved_text))
- .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0))
- .setWhen(now)
- .setAutoCancel(true)
- .setColor(r.getColor(
- com.android.internal.R.color.system_notification_accent_color));
-
- mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build());
-
- Notification n = mNotificationBuilder.build();
- n.flags &= ~Notification.FLAG_NO_CLEAR;
- mNotificationManager.notify(mNotificationId, n);
+ mNotificationManager.notify(R.id.notification_screenshot, mNotificationBuilder.build());
}
params.finisher.run();
params.clearContext();
}
+
+ @Override
+ protected void onCancelled(SaveImageInBackgroundData params) {
+ params.finisher.run();
+ params.clearImage();
+ params.clearContext();
+
+ // Cancel the posted notification
+ mNotificationManager.cancel(R.id.notification_screenshot);
+ }
}
/**
@@ -379,16 +379,7 @@
}
}
-/**
- * TODO:
- * - Performance when over gl surfaces? Ie. Gallery
- * - what do we say in the Toast? Which icon do we get if the user uses another
- * type of gallery?
- */
class GlobalScreenshot {
- private static final String TAG = "GlobalScreenshot";
-
- static final String CANCEL_ID = "android:cancel_id";
static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130;
@@ -512,8 +503,8 @@
if (mSaveInBgTask != null) {
mSaveInBgTask.cancel(false);
}
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager,
- R.id.notification_screenshot).execute(data);
+ mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager)
+ .execute(data);
}
/**
@@ -553,7 +544,8 @@
// Take the screenshot
mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
if (mScreenBitmap == null) {
- notifyScreenshotError(mContext, mNotificationManager);
+ notifyScreenshotError(mContext, mNotificationManager,
+ R.string.screenshot_failed_to_capture_text);
finisher.run();
return;
}
@@ -763,14 +755,15 @@
return anim;
}
- static void notifyScreenshotError(Context context, NotificationManager nManager) {
+ static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) {
Resources r = context.getResources();
+ String errorMsg = r.getString(msgResId);
- // Clear all existing notification, compose the new notification and show it
+ // Repurpose the existing notification to notify the user of the error
Notification.Builder b = new Notification.Builder(context)
.setTicker(r.getString(R.string.screenshot_failed_title))
.setContentTitle(r.getString(R.string.screenshot_failed_title))
- .setContentText(r.getString(R.string.screenshot_failed_text))
+ .setContentText(errorMsg)
.setSmallIcon(R.drawable.stat_notify_image_error)
.setWhen(System.currentTimeMillis())
.setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
@@ -778,9 +771,9 @@
.setAutoCancel(true)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color));
- Notification n =
- new Notification.BigTextStyle(b)
- .bigText(r.getString(R.string.screenshot_failed_text))
+
+ Notification n = new Notification.BigTextStyle(b)
+ .bigText(errorMsg)
.build();
nManager.notify(R.id.notification_screenshot, n);
}
@@ -791,15 +784,10 @@
public static class TargetChosenReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!intent.hasExtra(CANCEL_ID)) {
- return;
- }
-
// Clear the notification
final NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- final int id = intent.getIntExtra(CANCEL_ID, 0);
- nm.cancel(id);
+ nm.cancel(R.id.notification_screenshot);
}
}
@@ -809,16 +797,15 @@
public static class DeleteScreenshotReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!intent.hasExtra(CANCEL_ID) || !intent.hasExtra(SCREENSHOT_URI_ID)) {
+ if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
return;
}
// Clear the notification
final NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- final int id = intent.getIntExtra(CANCEL_ID, 0);
final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
- nm.cancel(id);
+ nm.cancel(R.id.notification_screenshot);
// And delete the image from the media store
new DeleteImageInBackgroundTask(context).execute(uri);