From 9091f323bbc83b8a98aa1226eb8d288b9ac6b88e Mon Sep 17 00:00:00 2001 From: Paul Stewart Date: Wed, 11 Nov 2015 10:23:43 -0800 Subject: WifiEnterpriseConfiguration: Do not print credentials in toString BUG:25624963 Change-Id: I939a12a27d6b915d8a9cc8b142f645fba0ee42ec --- wifi/java/android/net/wifi/WifiEnterpriseConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java index 59b22bd..16035ad 100644 --- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java +++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java @@ -768,7 +768,9 @@ public class WifiEnterpriseConfig implements Parcelable { public String toString() { StringBuffer sb = new StringBuffer(); for (String key : mFields.keySet()) { - sb.append(key).append(" ").append(mFields.get(key)).append("\n"); + // Don't display password in toString(). + String value = (key == PASSWORD_KEY) ? "" : mFields.get(key); + sb.append(key).append(" ").append(value).append("\n"); } return sb.toString(); } -- cgit v1.1 From 2e40f264cfc18d2ad7e5facad077f5498473fb89 Mon Sep 17 00:00:00 2001 From: Paul Stewart Date: Mon, 21 Mar 2016 09:51:27 -0700 Subject: Fix string equality comparison Don't use "==" to compare strings. Bug: 25624963 Change-Id: Id25696e4fdcbcf4d48ec74e8ed65c1a33716b30c --- wifi/java/android/net/wifi/WifiEnterpriseConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java index 16035ad..a337755 100644 --- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java +++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java @@ -769,7 +769,7 @@ public class WifiEnterpriseConfig implements Parcelable { StringBuffer sb = new StringBuffer(); for (String key : mFields.keySet()) { // Don't display password in toString(). - String value = (key == PASSWORD_KEY) ? "" : mFields.get(key); + String value = PASSWORD_KEY.equals(key) ? "" : mFields.get(key); sb.append(key).append(" ").append(value).append("\n"); } return sb.toString(); -- cgit v1.1 From 135ea8b3263a0dd58cd5a83c33fad6f2f6c5b4f6 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 28 Jun 2016 15:16:21 -0400 Subject: Check caller's uid before allowing notification policy access. Bug: 29421441 Change-Id: I7460268595e932d54660b02007bcd68b95fe8aec --- .../com/android/server/notification/NotificationManagerService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 4351798..b922d26 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1642,6 +1642,7 @@ public class NotificationManagerService extends SystemService { } private void enforcePolicyAccess(String pkg, String method) { + checkCallerIsSameApp(pkg); if (!checkPolicyAccess(pkg)) { Slog.w(TAG, "Notification policy access denied calling " + method); throw new SecurityException("Notification policy access denied"); @@ -3130,6 +3131,10 @@ public class NotificationManagerService extends SystemService { if (isCallerSystem()) { return; } + checkCallerIsSameApp(pkg); + } + + private static void checkCallerIsSameApp(String pkg) { final int uid = Binder.getCallingUid(); try { ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo( -- cgit v1.1 From 637501e9f6fdade50e2e64a227c402dc9e7f301d Mon Sep 17 00:00:00 2001 From: Sergio Giro Date: Tue, 28 Jun 2016 18:26:10 +0100 Subject: Add bound checks to utf16_to_utf8 Test: ran libaapt2_tests64 Bug: 29250543 Change-Id: I1ebc017af623b6514cf0c493e8cd8e1d59ea26c3 (cherry picked from commit 4781057e78f63e0e99af109cebf3b6a78f4bfbb6) --- tools/aapt/Android.mk | 2 +- tools/aapt2/Util.cpp | 4 +++- tools/split-select/Android.mk | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk index b991d55..08cbd5a 100644 --- a/tools/aapt/Android.mk +++ b/tools/aapt/Android.mk @@ -63,8 +63,8 @@ aaptHostLdLibs := aaptHostStaticLibs := \ libandroidfw \ libpng \ - liblog \ libutils \ + liblog \ libcutils \ libexpat \ libziparchive-host \ diff --git a/tools/aapt2/Util.cpp b/tools/aapt2/Util.cpp index 03ecd1a..5b064e3 100644 --- a/tools/aapt2/Util.cpp +++ b/tools/aapt2/Util.cpp @@ -303,8 +303,10 @@ std::string utf16ToUtf8(const StringPiece16& utf16) { } std::string utf8; + // Make room for '\0' explicitly. + utf8.resize(utf8Length + 1); + utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8Length + 1); utf8.resize(utf8Length); - utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin()); return utf8; } diff --git a/tools/split-select/Android.mk b/tools/split-select/Android.mk index d9ddf08..54f0a1f 100644 --- a/tools/split-select/Android.mk +++ b/tools/split-select/Android.mk @@ -48,8 +48,8 @@ hostStaticLibs := \ libaapt \ libandroidfw \ libpng \ - liblog \ libutils \ + liblog \ libcutils \ libexpat \ libziparchive-host \ -- cgit v1.1 From a2041d6b45615c076a182da088d6ca1e0c53ee40 Mon Sep 17 00:00:00 2001 From: Benjamin Franz Date: Tue, 12 Jul 2016 15:20:47 +0100 Subject: DO NOT MERGE Block the user from entering safe boot mode Block the user from entering safe boot mode if the DISALLOW_SAFE_BOOT policy is set. Bug: 26251884 Change-Id: I4945d5d676928346c11ea305a5b6a2e1a42e94e6 --- services/core/java/com/android/server/wm/WindowManagerService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 71bbdb6..d695d93 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -65,6 +65,7 @@ import android.os.SystemProperties; import android.os.SystemService; import android.os.Trace; import android.os.UserHandle; +import android.os.UserManager; import android.os.WorkSource; import android.provider.Settings; import android.util.ArraySet; @@ -7613,6 +7614,12 @@ public class WindowManagerService extends IWindowManager.Stub + " milliseconds before attempting to detect safe mode."); } + UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + if (um != null && um.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) { + mSafeMode = false; + return false; + } + int menuState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_ANY, KeyEvent.KEYCODE_MENU); int sState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_ANY, KeyEvent.KEYCODE_S); -- cgit v1.1 From efec38abd105b64c8221157a524f30e9eeafe356 Mon Sep 17 00:00:00 2001 From: Sungsoo Lim Date: Wed, 13 Jul 2016 09:31:16 +0900 Subject: DO NOT MERGE: Remove the use of JHEAD in ExifInterface Bug: 29270469 Change-Id: I6a6c8aeab2a842ff1646316363d614851625e78f --- media/java/android/media/ExifInterface.java | 2451 ++++++++++++++++++++++++--- media/jni/Android.mk | 4 - 2 files changed, 2258 insertions(+), 197 deletions(-) diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 6bf5721..c5e978f 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -16,85 +16,307 @@ package android.media; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.util.Pair; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +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.FilterOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +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.HashSet; import java.util.Map; +import java.util.Set; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This is a class for reading and writing Exif tags in a JPEG file. */ 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"; + /** Type is String. @hide */ + public static final String TAG_ARTIST = "Artist"; + /** Type is int. @hide */ + public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample"; + /** Type is int. @hide */ + public static final String TAG_COMPRESSION = "Compression"; + /** Type is String. @hide */ + public static final String TAG_COPYRIGHT = "Copyright"; /** Type is String. */ public static final String TAG_DATETIME = "DateTime"; - /** Type is String. */ - public static final String TAG_MAKE = "Make"; - /** Type is String. */ - public static final String TAG_MODEL = "Model"; + /** Type is String. @hide */ + public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription"; /** Type is int. */ - public static final String TAG_FLASH = "Flash"; + public static final String TAG_IMAGE_LENGTH = "ImageLength"; /** Type is int. */ public static final String TAG_IMAGE_WIDTH = "ImageWidth"; - /** Type is int. */ - public static final String TAG_IMAGE_LENGTH = "ImageLength"; - /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */ - public static final String TAG_GPS_LATITUDE = "GPSLatitude"; - /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */ - public static final String TAG_GPS_LONGITUDE = "GPSLongitude"; + /** Type is int. @hide */ + public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat"; + /** Type is int. @hide */ + public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength"; /** Type is String. */ - public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + public static final String TAG_MAKE = "Make"; /** Type is String. */ - public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + public static final String TAG_MODEL = "Model"; + /** Type is int. */ + public static final String TAG_ORIENTATION = "Orientation"; + /** Type is int. @hide */ + public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; + /** Type is int. @hide */ + public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration"; + /** Type is rational. @hide */ + public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities"; + /** Type is rational. @hide */ + public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite"; + /** Type is int. @hide */ + public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit"; + /** Type is int. @hide */ + public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip"; + /** Type is int. @hide */ + public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel"; + /** Type is String. @hide */ + public static final String TAG_SOFTWARE = "Software"; + /** Type is int. @hide */ + public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts"; + /** Type is int. @hide */ + public static final String TAG_STRIP_OFFSETS = "StripOffsets"; + /** Type is int. @hide */ + public static final String TAG_TRANSFER_FUNCTION = "TransferFunction"; + /** Type is rational. @hide */ + public static final String TAG_WHITE_POINT = "WhitePoint"; + /** Type is rational. @hide */ + public static final String TAG_X_RESOLUTION = "XResolution"; + /** Type is rational. @hide */ + public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients"; + /** Type is int. @hide */ + public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning"; + /** Type is int. @hide */ + public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling"; + /** Type is rational. @hide */ + public static final String TAG_Y_RESOLUTION = "YResolution"; + /** Type is rational. @hide */ + public static final String TAG_APERTURE_VALUE = "ApertureValue"; + /** Type is rational. @hide */ + public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue"; + /** Type is String. @hide */ + public static final String TAG_CFA_PATTERN = "CFAPattern"; + /** Type is int. @hide */ + public static final String TAG_COLOR_SPACE = "ColorSpace"; + /** Type is String. @hide */ + public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration"; + /** Type is rational. @hide */ + public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel"; + /** Type is int. @hide */ + public static final String TAG_CONTRAST = "Contrast"; + /** Type is int. @hide */ + public static final String TAG_CUSTOM_RENDERED = "CustomRendered"; /** Type is String. */ + public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized"; + /** Type is String. @hide */ + public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal"; + /** Type is String. @hide */ + public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription"; + /** Type is double. @hide */ + public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio"; + /** Type is String. @hide */ + public static final String TAG_EXIF_VERSION = "ExifVersion"; + /** Type is double. @hide */ + public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue"; + /** Type is rational. @hide */ + public static final String TAG_EXPOSURE_INDEX = "ExposureIndex"; + /** Type is int. @hide */ + public static final String TAG_EXPOSURE_MODE = "ExposureMode"; + /** Type is int. @hide */ + public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram"; + /** Type is double. */ public static final String TAG_EXPOSURE_TIME = "ExposureTime"; - /** Type is String. */ + /** Type is double. */ public static final String TAG_APERTURE = "FNumber"; - /** Type is String. */ + /** Type is String. @hide */ + public static final String TAG_FILE_SOURCE = "FileSource"; + /** Type is int. */ + public static final String TAG_FLASH = "Flash"; + /** Type is rational. @hide */ + public static final String TAG_FLASH_ENERGY = "FlashEnergy"; + /** Type is String. @hide */ + public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion"; + /** Type is rational. */ + public static final String TAG_FOCAL_LENGTH = "FocalLength"; + /** Type is int. @hide */ + public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm"; + /** Type is int. @hide */ + public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit"; + /** Type is rational. @hide */ + public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution"; + /** Type is rational. @hide */ + public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution"; + /** Type is int. @hide */ + public static final String TAG_GAIN_CONTROL = "GainControl"; + /** Type is int. */ public static final String TAG_ISO = "ISOSpeedRatings"; + /** Type is String. @hide */ + public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID"; + /** Type is int. @hide */ + public static final String TAG_LIGHT_SOURCE = "LightSource"; + /** Type is String. @hide */ + public static final String TAG_MAKER_NOTE = "MakerNote"; + /** Type is rational. @hide */ + public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue"; + /** Type is int. @hide */ + public static final String TAG_METERING_MODE = "MeteringMode"; + /** Type is String. @hide */ + public static final String TAG_OECF = "OECF"; + /** Type is int. @hide */ + public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension"; + /** Type is int. @hide */ + public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension"; + /** Type is String. @hide */ + public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile"; + /** Type is int. @hide */ + public static final String TAG_SATURATION = "Saturation"; + /** Type is int. @hide */ + public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType"; + /** Type is String. @hide */ + public static final String TAG_SCENE_TYPE = "SceneType"; + /** Type is int. @hide */ + public static final String TAG_SENSING_METHOD = "SensingMethod"; + /** Type is int. @hide */ + public static final String TAG_SHARPNESS = "Sharpness"; + /** Type is rational. @hide */ + public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue"; + /** Type is String. @hide */ + public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse"; + /** Type is String. @hide */ + public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity"; /** Type is String. */ - public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized"; - /** Type is int. */ public static final String TAG_SUBSEC_TIME = "SubSecTime"; - /** Type is int. */ + /** Type is String. */ + public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized"; + /** Type is String. */ public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_AREA = "SubjectArea"; + /** Type is double. @hide */ + public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_LOCATION = "SubjectLocation"; + /** Type is String. @hide */ + public static final String TAG_USER_COMMENT = "UserComment"; /** Type is int. */ - public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized"; - - - - /** - * @hide - */ - public static final String TAG_SUBSECTIME = "SubSecTime"; - + public static final String TAG_WHITE_BALANCE = "WhiteBalance"; /** * The altitude (in meters) based on the reference in TAG_GPS_ALTITUDE_REF. * Type is rational. */ public static final String TAG_GPS_ALTITUDE = "GPSAltitude"; - /** * 0 if the altitude is above sea level. 1 if the altitude is below sea * level. Type is int. */ public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef"; - - /** Type is String. */ - public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp"; + /** Type is String. @hide */ + public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DOP = "GPSDOP"; /** Type is String. */ public static final String TAG_GPS_DATESTAMP = "GPSDateStamp"; - /** Type is int. */ - public static final String TAG_WHITE_BALANCE = "WhiteBalance"; - /** Type is rational. */ - public static final String TAG_FOCAL_LENGTH = "FocalLength"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef"; + /** Type is int. @hide */ + public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential"; + /** Type is rational. @hide */ + public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; + /** Type is String. @hide */ + public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */ + public static final String TAG_GPS_LATITUDE = "GPSLatitude"; + /** Type is String. */ + public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */ + public static final String TAG_GPS_LONGITUDE = "GPSLongitude"; + /** Type is String. */ + public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum"; + /** Type is String. @hide */ + public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode"; /** Type is String. Name of GPS processing method used for location finding. */ public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod"; + /** Type is String. @hide */ + public static final String TAG_GPS_SATELLITES = "GPSSatellites"; + /** Type is rational. @hide */ + public static final String TAG_GPS_SPEED = "GPSSpeed"; + /** Type is String. @hide */ + public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_STATUS = "GPSStatus"; + /** Type is String. Format is "hh:mm:ss". */ + public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp"; + /** Type is rational. @hide */ + public static final String TAG_GPS_TRACK = "GPSTrack"; + /** Type is String. @hide */ + public static final String TAG_GPS_TRACK_REF = "GPSTrackRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_VERSION_ID = "GPSVersionID"; + /** Type is String. @hide */ + public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex"; + /** Type is int. @hide */ + public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength"; + /** Type is int. @hide */ + public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth"; + + // Private tags used for pointing the other IFD offset. The types of the following tags are int. + private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer"; + private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer"; + private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer"; + + // Private tags used for thumbnail information. + private static final String TAG_HAS_THUMBNAIL = "HasThumbnail"; + private static final String TAG_THUMBNAIL_OFFSET = "ThumbnailOffset"; + private static final String TAG_THUMBNAIL_LENGTH = "ThumbnailLength"; + private static final String TAG_THUMBNAIL_DATA = "ThumbnailData"; // Constants used for the Orientation Exif tag. public static final int ORIENTATION_UNDEFINED = 0; @@ -102,34 +324,730 @@ public class ExifInterface { public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // left right reversed mirror public static final int ORIENTATION_ROTATE_180 = 3; public static final int ORIENTATION_FLIP_VERTICAL = 4; // upside down mirror - public static final int ORIENTATION_TRANSPOSE = 5; // flipped about top-left <--> bottom-right axis + // flipped about top-left <--> bottom-right axis + public static final int ORIENTATION_TRANSPOSE = 5; public static final int ORIENTATION_ROTATE_90 = 6; // rotate 90 cw to right it - public static final int ORIENTATION_TRANSVERSE = 7; // flipped about top-right <--> bottom-left axis + // flipped about top-right <--> bottom-left axis + public static final int ORIENTATION_TRANSVERSE = 7; public static final int ORIENTATION_ROTATE_270 = 8; // rotate 270 to right it // 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; + // Names for the data formats for debugging purpose. + private static final String[] IFD_FORMAT_NAMES = new String[] { + "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT", + "SLONG", "SRATIONAL", "SINGLE", "DOUBLE" + }; + // 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 rational type. + private static class Rational { + public final long numerator; + public final long denominator; + + private Rational(long numerator, long denominator) { + // Handle erroneous case + if (denominator == 0) { + this.numerator = 0; + this.denominator = 1; + return; + } + this.numerator = numerator; + this.denominator = denominator; + } + + @Override + public String toString() { + return numerator + "/" + denominator; + } + + public double calculate() { + return (double) numerator / denominator; + } + } + + // A class for indicating EXIF attribute. + private static class ExifAttribute { + public final int format; + public final int numberOfComponents; + public final byte[] bytes; + + private ExifAttribute(int format, int numberOfComponents, byte[] bytes) { + this.format = format; + this.numberOfComponents = numberOfComponents; + this.bytes = bytes; + } + + public static ExifAttribute createUShort(int[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]); + buffer.order(byteOrder); + for (int value : values) { + buffer.putShort((short) value); + } + return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array()); + } + + public static ExifAttribute createUShort(int value, ByteOrder byteOrder) { + return createUShort(new int[] {value}, byteOrder); + } + + public static ExifAttribute createULong(long[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]); + buffer.order(byteOrder); + for (long value : values) { + buffer.putInt((int) value); + } + return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array()); + } + + public static ExifAttribute createULong(long value, ByteOrder byteOrder) { + return createULong(new long[] {value}, byteOrder); + } + + public static ExifAttribute createSLong(int[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]); + buffer.order(byteOrder); + for (int value : values) { + buffer.putInt(value); + } + return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array()); + } + + public static ExifAttribute createSLong(int value, ByteOrder byteOrder) { + return createSLong(new int[] {value}, byteOrder); + } + + public static ExifAttribute createByte(String value) { + // Exception for GPSAltitudeRef tag + if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') { + final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') }; + return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes); + } + final byte[] ascii = value.getBytes(ASCII); + return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii); + } + + public static ExifAttribute createString(String value) { + final byte[] ascii = (value + '\0').getBytes(ASCII); + return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii); + } + + public static ExifAttribute createURational(Rational[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]); + buffer.order(byteOrder); + for (Rational value : values) { + buffer.putInt((int) value.numerator); + buffer.putInt((int) value.denominator); + } + return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array()); + } + + public static ExifAttribute createURational(Rational value, ByteOrder byteOrder) { + return createURational(new Rational[] {value}, byteOrder); + } + + public static ExifAttribute createSRational(Rational[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]); + buffer.order(byteOrder); + for (Rational value : values) { + buffer.putInt((int) value.numerator); + buffer.putInt((int) value.denominator); + } + return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array()); + } + + public static ExifAttribute createSRational(Rational value, ByteOrder byteOrder) { + return createSRational(new Rational[] {value}, byteOrder); + } + + public static ExifAttribute createDouble(double[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]); + buffer.order(byteOrder); + for (double value : values) { + buffer.putDouble(value); + } + return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array()); + } + + public static ExifAttribute createDouble(double value, ByteOrder byteOrder) { + return createDouble(new double[] {value}, byteOrder); + } + + @Override + public String toString() { + return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")"; + } + + private Object getValue(ByteOrder byteOrder) { + try { + ByteOrderAwarenessDataInputStream inputStream = + new ByteOrderAwarenessDataInputStream(bytes); + inputStream.setByteOrder(byteOrder); + switch (format) { + case IFD_FORMAT_BYTE: + case IFD_FORMAT_SBYTE: { + // Exception for GPSAltitudeRef tag + if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) { + return new String(new char[] { (char) (bytes[0] + '0') }); + } + return new String(bytes, ASCII); + } + case IFD_FORMAT_UNDEFINED: + case IFD_FORMAT_STRING: { + 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 (index < numberOfComponents) { + int ch = bytes[index]; + if (ch == 0) { + break; + } + if (ch >= 32) { + stringBuilder.append((char) ch); + } else { + stringBuilder.append('?'); + } + ++index; + } + return stringBuilder.toString(); + } + case IFD_FORMAT_USHORT: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readUnsignedShort(); + } + return values; + } + case IFD_FORMAT_ULONG: { + final long[] values = new long[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readUnsignedInt(); + } + return values; + } + case IFD_FORMAT_URATIONAL: { + final Rational[] values = new Rational[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + final long numerator = inputStream.readUnsignedInt(); + final long denominator = inputStream.readUnsignedInt(); + values[i] = new Rational(numerator, denominator); + } + return values; + } + case IFD_FORMAT_SSHORT: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readShort(); + } + return values; + } + case IFD_FORMAT_SLONG: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readInt(); + } + return values; + } + case IFD_FORMAT_SRATIONAL: { + final Rational[] values = new Rational[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + final long numerator = inputStream.readInt(); + final long denominator = inputStream.readInt(); + values[i] = new Rational(numerator, denominator); + } + return values; + } + case IFD_FORMAT_SINGLE: { + final double[] values = new double[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readFloat(); + } + return values; + } + case IFD_FORMAT_DOUBLE: { + final double[] values = new double[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readDouble(); + } + return values; + } + default: + return null; + } + } catch (IOException e) { + Log.w(TAG, "IOException occurred during reading a value", e); + return null; + } + } + + public double getDoubleValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + throw new NumberFormatException("NULL can't be converted to a double value"); + } + if (value instanceof String) { + return Double.parseDouble((String) value); + } + if (value instanceof long[]) { + long[] array = (long[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof double[]) { + double[] array = (double[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof Rational[]) { + Rational[] array = (Rational[]) value; + if (array.length == 1) { + return array[0].calculate(); + } + throw new NumberFormatException("There are more than one component"); + } + throw new NumberFormatException("Couldn't find a double value"); + } + + public int getIntValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + throw new NumberFormatException("NULL can't be converted to a integer value"); + } + if (value instanceof String) { + return Integer.parseInt((String) value); + } + if (value instanceof long[]) { + long[] array = (long[]) value; + if (array.length == 1) { + return (int) array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + throw new NumberFormatException("Couldn't find a integer value"); + } + + public String getStringValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + return null; + } + if (value instanceof String) { + return (String) value; + } + + final StringBuilder stringBuilder = new StringBuilder(); + if (value instanceof long[]) { + long[] array = (long[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof double[]) { + double[] array = (double[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof Rational[]) { + Rational[] array = (Rational[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i].numerator); + stringBuilder.append('/'); + stringBuilder.append(array[i].denominator); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + return null; + } + + public int size() { + return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents; + } + } + + // A class for indicating EXIF tag. + private static class ExifTag { + public final int number; + public final String name; + public final int primaryFormat; + public final int secondaryFormat; + + private ExifTag(String name, int number, int format) { + this.name = name; + this.number = number; + this.primaryFormat = format; + this.secondaryFormat = -1; + } + + private ExifTag(String name, int number, int primaryFormat, int secondaryFormat) { + this.name = name; + this.number = number; + this.primaryFormat = primaryFormat; + this.secondaryFormat = secondaryFormat; + } + } + + // Primary image IFD TIFF tags (See JEITA CP-3451 Table 14. page 54). + private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] { + new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), + new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), + new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), + new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), + new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), + new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), + new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), + new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), + new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), + new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), + new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), + new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), + new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), + new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), + new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), + new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), + new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + }; + + // 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(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_APERTURE, 33437, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT), + new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING), + new ExifTag(TAG_ISO, 34855, IFD_FORMAT_USHORT), + new ExifTag(TAG_OECF, 34856, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING), + new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT), + new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT), + new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT), + new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT), + new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING), + new ExifTag(TAG_SUBSEC_TIME_ORIG, 37521, IFD_FORMAT_STRING), + new ExifTag(TAG_SUBSEC_TIME_DIG, 37522, IFD_FORMAT_STRING), + new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT), + new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING), + new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG), + new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT), + new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT), + new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT), + new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT), + new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT), + new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT), + new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT), + new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT), + new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT), + new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT), + new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT), + new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT), + new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING), + }; + + // 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(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE), + new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE), + new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT), + }; + // Primary image IFD Interoperability tag (See JEITA CP-3451 Table 17. page 56). + private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] { + new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING), + }; + // IFD Thumbnail tags (See JEITA CP-3451 Table 18. page 57). + private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] { + new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), + new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), + new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), + new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), + new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), + new ExifTag(TAG_STRIP_OFFSETS, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), + new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), + new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), + new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), + new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), + new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), + new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), + new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), + new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), + new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), + new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), + new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + }; + + // 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(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG), + }; + // 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(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG); + private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG = + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG); + + // 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]; + // Mappings from tag name to tag number and each item represents one IFD tag group. + private static final HashMap[] sExifTagMapsForWriting = new HashMap[EXIF_TAGS.length]; + private static final HashSet sTagSetForCompatibility = new HashSet<>(Arrays.asList( + TAG_APERTURE, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE, + TAG_GPS_TIMESTAMP)); + + // 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. + + private static final Charset ASCII = Charset.forName("US-ASCII"); + // Identifier for EXIF APP1 segment in JPEG + private static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(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"); 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(); + sExifTagMapsForWriting[hint] = new HashMap(); + for (ExifTag tag : EXIF_TAGS[hint]) { + sExifTagMapsForReading[hint].put(tag.number, tag); + sExifTagMapsForWriting[hint].put(tag.name, tag); + } + } } - private String mFilename; - private HashMap mAttributes; + private final String mFilename; + private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length]; + private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN; private boolean mHasThumbnail; + // The following values used for indicating a thumbnail position. + private int mThumbnailOffset; + private int mThumbnailLength; + private byte[] mThumbnailBytes; - // 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(); + // Pattern to check non zero timestamp + private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*"); + // Pattern to check gps timestamp + private static final Pattern sGpsTimestampPattern = + Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"); /** - * Reads Exif tags from the specified JPEG file. + * Reads Exif tags from the specified image file. */ public ExifInterface(String filename) throws IOException { if (filename == null) { @@ -139,53 +1057,99 @@ public class ExifInterface { loadAttributes(); } + + /** + * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in + * the image file. + * + * @param tag the name of the tag. + */ + private ExifAttribute getExifAttribute(String tag) { + // Retrieves all tag groups. The value from primary image tag group has a higher priority + // than the value from the thumbnail tag group if there are more than one candidates. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + Object value = mAttributes[i].get(tag); + if (value != null) { + return (ExifAttribute) value; + } + } + return null; + } + /** * Returns the value of the specified tag or {@code null} if there - * is no such tag in the JPEG file. + * is no such tag in the image file. * * @param tag the name of the tag. */ public String getAttribute(String tag) { - return mAttributes.get(tag); + ExifAttribute attribute = getExifAttribute(tag); + if (attribute != null) { + if (!sTagSetForCompatibility.contains(tag)) { + return attribute.getStringValue(mExifByteOrder); + } + if (tag.equals(TAG_GPS_TIMESTAMP)) { + // Convert the rational values to the custom formats for backwards compatibility. + if (attribute.format != IFD_FORMAT_URATIONAL + && attribute.format != IFD_FORMAT_SRATIONAL) { + return null; + } + Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder); + if (array.length != 3) { + return null; + } + return String.format("%02d:%02d:%02d", + (int) ((float) array[0].numerator / array[0].denominator), + (int) ((float) array[1].numerator / array[1].denominator), + (int) ((float) array[2].numerator / array[2].denominator)); + } + try { + return Double.toString(attribute.getDoubleValue(mExifByteOrder)); + } catch (NumberFormatException e) { + return null; + } + } + return null; } /** * Returns the integer value of the specified tag. If there is no such tag - * in the JPEG file or the value cannot be parsed as integer, return + * in the image file or the value cannot be parsed as integer, return * defaultValue. * * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ public int getAttributeInt(String tag, int defaultValue) { - String value = mAttributes.get(tag); - if (value == null) return defaultValue; + ExifAttribute exifAttribute = getExifAttribute(tag); + if (exifAttribute == null) { + return defaultValue; + } + try { - return Integer.valueOf(value); - } catch (NumberFormatException ex) { + return exifAttribute.getIntValue(mExifByteOrder); + } catch (NumberFormatException e) { return defaultValue; } } /** - * Returns the double value of the specified rational tag. If there is no - * such tag in the JPEG file or the value cannot be parsed as double, return - * defaultValue. + * 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 defaultValue. * * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ public double getAttributeDouble(String tag, double defaultValue) { - String value = mAttributes.get(tag); - if (value == null) return defaultValue; + ExifAttribute exifAttribute = getExifAttribute(tag); + if (exifAttribute == null) { + return defaultValue; + } + try { - int index = value.indexOf("/"); - if (index == -1) return defaultValue; - double denom = Double.parseDouble(value.substring(index + 1)); - if (denom == 0) return defaultValue; - double num = Double.parseDouble(value.substring(0, index)); - return num / denom; - } catch (NumberFormatException ex) { + return exifAttribute.getDoubleValue(mExifByteOrder); + } catch (NumberFormatException e) { return defaultValue; } } @@ -197,126 +1161,299 @@ public class ExifInterface { * @param value the value of the tag. */ public void setAttribute(String tag, String value) { - mAttributes.put(tag, value); - } - - /** - * Initialize mAttributes with the attributes from the file mFilename. - * - * mAttributes is a HashMap which stores the Exif attributes of the file. - * The key is the standard tag name and the value is the tag's value: e.g. - * Model -> Nikon. Numeric values are stored as strings. - * - * This function also initialize mHasThumbnail to indicate whether the - * file has a thumbnail inside. - */ - private void loadAttributes() throws IOException { - // 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" - mAttributes = new HashMap(); - - 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("hasThumbnail")) { - mHasThumbnail = attrValue.equalsIgnoreCase("true"); + // Convert the given value to rational values for backwards compatibility. + if (value != null && sTagSetForCompatibility.contains(tag)) { + if (tag.equals(TAG_GPS_TIMESTAMP)) { + Matcher m = sGpsTimestampPattern.matcher(value); + if (!m.find()) { + Log.w(TAG, "Invalid value for " + tag + " : " + value); + return; + } + value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1," + + Integer.parseInt(m.group(3)) + "/1"; } else { - mAttributes.put(attrName, attrValue); + try { + double doubleValue = Double.parseDouble(value); + value = (long) (doubleValue * 10000L) + "/10000"; + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid value for " + tag + " : " + value); + return; + } } } - } - /** - * Save the tag data into the JPEG file. This is expensive because it involves - * copying all the JPG data from one file to another and deleting the old file - * and renaming the other. It's best to use {@link #setAttribute(String,String)} - * to set all attributes to write and make a single call rather than multiple - * calls for each attribute. - */ - public void saveAttributes() throws IOException { - // 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("hasThumbnail")) { - --size; - } - sb.append(size + " "); - for (Map.Entry iter : mAttributes.entrySet()) { - String key = iter.getKey(); - if (key.equals("hasThumbnail")) { - // this is a fake attribute not saved as an exif tag + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + if (i == IFD_THUMBNAIL_HINT && !mHasThumbnail) { continue; } - String val = iter.getValue(); - sb.append(key + "="); - sb.append(val.length() + " "); - sb.append(val); - } - String s = sb.toString(); - synchronized (sLock) { - saveAttributesNative(mFilename, s); - commitChangesNative(mFilename); + final Object obj = sExifTagMapsForWriting[i].get(tag); + if (obj != null) { + if (value == null) { + mAttributes[i].remove(tag); + continue; + } + final ExifTag exifTag = (ExifTag) obj; + Pair guess = guessDataFormat(value); + int dataFormat; + if (exifTag.primaryFormat == guess.first || exifTag.primaryFormat == guess.second) { + dataFormat = exifTag.primaryFormat; + } else if (exifTag.secondaryFormat != -1 && (exifTag.secondaryFormat == guess.first + || exifTag.secondaryFormat == guess.second)) { + dataFormat = exifTag.secondaryFormat; + } else if (exifTag.primaryFormat == IFD_FORMAT_BYTE + || exifTag.primaryFormat == IFD_FORMAT_UNDEFINED + || exifTag.primaryFormat == IFD_FORMAT_STRING) { + dataFormat = exifTag.primaryFormat; + } else { + Log.w(TAG, "Given tag (" + tag + ") value didn't match with one of expected " + + "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat] + + (exifTag.secondaryFormat == -1 ? "" : ", " + + IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: " + + IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? "" : ", " + + IFD_FORMAT_NAMES[guess.second]) + ")"); + continue; + } + switch (dataFormat) { + case IFD_FORMAT_BYTE: { + mAttributes[i].put(tag, ExifAttribute.createByte(value)); + break; + } + case IFD_FORMAT_UNDEFINED: + case IFD_FORMAT_STRING: { + mAttributes[i].put(tag, ExifAttribute.createString(value)); + break; + } + case IFD_FORMAT_USHORT: { + final String[] values = value.split(","); + final int[] intArray = new int[values.length]; + for (int j = 0; j < values.length; ++j) { + intArray[j] = Integer.parseInt(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createUShort(intArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_SLONG: { + final String[] values = value.split(","); + final int[] intArray = new int[values.length]; + for (int j = 0; j < values.length; ++j) { + intArray[j] = Integer.parseInt(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createSLong(intArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_ULONG: { + final String[] values = value.split(","); + final long[] longArray = new long[values.length]; + for (int j = 0; j < values.length; ++j) { + longArray[j] = Long.parseLong(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createULong(longArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_URATIONAL: { + final String[] values = value.split(","); + final Rational[] rationalArray = new Rational[values.length]; + for (int j = 0; j < values.length; ++j) { + final String[] numbers = values[j].split("/"); + rationalArray[j] = new Rational(Long.parseLong(numbers[0]), + Long.parseLong(numbers[1])); + } + mAttributes[i].put(tag, + ExifAttribute.createURational(rationalArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_SRATIONAL: { + final String[] values = value.split(","); + final Rational[] rationalArray = new Rational[values.length]; + for (int j = 0; j < values.length; ++j) { + final String[] numbers = values[j].split("/"); + rationalArray[j] = new Rational(Long.parseLong(numbers[0]), + Long.parseLong(numbers[1])); + } + mAttributes[i].put(tag, + ExifAttribute.createSRational(rationalArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_DOUBLE: { + final String[] values = value.split(","); + final double[] doubleArray = new double[values.length]; + for (int j = 0; j < values.length; ++j) { + doubleArray[j] = Double.parseDouble(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createDouble(doubleArray, mExifByteOrder)); + break; + } + default: + Log.w(TAG, "Data format isn't one of expected formats: " + dataFormat); + continue; + } + } } } /** - * Returns true if the JPEG file has a thumbnail. + * Update the values of the tags in the tag groups if any value for the tag already was stored. + * + * @param tag the name of the tag. + * @param value the value of the tag in a form of {@link ExifAttribute}. + * @return Returns {@code true} if updating is placed. */ - public boolean hasThumbnail() { - return mHasThumbnail; + private boolean updateAttribute(String tag, ExifAttribute value) { + boolean updated = false; + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + if (mAttributes[i].containsKey(tag)) { + mAttributes[i].put(tag, value); + updated = true; + } + } + return updated; } /** - * Returns the thumbnail inside the JPEG file, or {@code null} if there is no thumbnail. - * The returned data is in JPEG format and can be decoded using - * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} + * Remove any values of the specified tag. + * + * @param tag the name of the tag. */ - public byte[] getThumbnail() { - synchronized (sLock) { - return getThumbnailNative(mFilename); + private void removeAttribute(String tag) { + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + mAttributes[i].remove(tag); } } /** - * Returns the offset and length of thumbnail inside the JPEG file, or - * {@code null} if there is no thumbnail. - * - * @return two-element array, the offset in the first value, and length in - * the second, or {@code null} if no thumbnail was found. - * @hide + * This function decides which parser to read the image data according to the given input stream + * type and the content of the input stream. In each case, it reads the first three bytes to + * determine whether the image data format is JPEG or not. + */ + private void loadAttributes() throws IOException { + try { + InputStream in = new FileInputStream(mFilename); + // Initialize mAttributes. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + mAttributes[i] = new HashMap(); + } + getJpegAttributes(in); + } catch (IOException e) { + // Ignore exceptions in order to keep the compatibility with the old versions of + // ExifInterface. + Log.w(TAG, "Invalid image.", e); + } finally { + addDefaultValuesForCompatibility(); + if (DEBUG) { + printAttributes(); + } + } + } + + // Prints out attributes for debugging. + private void printAttributes() { + for (int i = 0; i < mAttributes.length; ++i) { + Log.d(TAG, "The size of tag group[" + i + "]: " + mAttributes[i].size()); + for (Map.Entry entry : (Set) mAttributes[i].entrySet()) { + final ExifAttribute tagValue = (ExifAttribute) entry.getValue(); + Log.d(TAG, "tagName: " + entry.getKey() + ", tagType: " + tagValue.toString() + + ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'"); + } + } + } + + /** + * Save the tag data into the original image file. This is expensive because it involves + * copying all the data from one file to another and deleting the old file and renaming the + * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write + * and make a single call rather than multiple calls for each attribute. + */ + public void saveAttributes() throws IOException { + // Keep the thumbnail in memory + mThumbnailBytes = getThumbnail(); + + File tempFile = null; + // Move the original file to temporary file. + tempFile = new File(mFilename + ".tmp"); + File originalFile = new File(mFilename); + if (!originalFile.renameTo(tempFile)) { + throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath()); + } + + FileInputStream in = null; + FileOutputStream out = null; + try { + // Save the new file. + in = new FileInputStream(tempFile); + out = new FileOutputStream(mFilename); + saveJpegAttributes(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + tempFile.delete(); + } + + // Discard the thumbnail in memory + mThumbnailBytes = null; + } + + /** + * Returns true if the image file has a thumbnail. + */ + public boolean hasThumbnail() { + return mHasThumbnail; + } + + /** + * Returns the thumbnail inside the image file, or {@code null} if there is no thumbnail. + * The returned data is in JPEG format and can be decoded using + * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} + */ + public byte[] getThumbnail() { + if (!mHasThumbnail) { + return null; + } + if (mThumbnailBytes != null) { + return mThumbnailBytes; + } + + // Read the thumbnail. + FileInputStream in = null; + try { + in = new FileInputStream(mFilename); + 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 e) { + // Couldn't get a thumbnail image. + } finally { + IoUtils.closeQuietly(in); + } + return null; + } + + /** + * Returns the offset and length of thumbnail inside the image file, or + * {@code null} if there is no thumbnail. + * + * @return two-element array, the offset in the first value, and length in + * the second, or {@code null} if no thumbnail was found. + * @hide */ public long[] getThumbnailRange() { - synchronized (sLock) { - return getThumbnailRangeNative(mFilename); + if (!mHasThumbnail) { + return null; } + + long[] range = new long[2]; + range[0] = mThumbnailOffset; + range[1] = mThumbnailLength; + + return range; } /** @@ -325,10 +1462,10 @@ public class ExifInterface { * 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 = getAttribute(TAG_GPS_LATITUDE); + String latRef = getAttribute(TAG_GPS_LATITUDE_REF); + String lngValue = getAttribute(TAG_GPS_LONGITUDE); + String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF); if (latValue != null && latRef != null && lngValue != null && lngRef != null) { try { @@ -354,7 +1491,7 @@ public class ExifInterface { 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; } @@ -366,8 +1503,9 @@ public class ExifInterface { * @hide */ public long getDateTime() { - String dateTimeString = mAttributes.get(TAG_DATETIME); - if (dateTimeString == null) return -1; + String dateTimeString = getAttribute(TAG_DATETIME); + if (dateTimeString == null + || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1; ParsePosition pos = new ParsePosition(0); try { @@ -377,7 +1515,7 @@ public class ExifInterface { if (datetime == null) return -1; long msecs = datetime.getTime(); - String subSecs = mAttributes.get(TAG_SUBSECTIME); + String subSecs = getAttribute(TAG_SUBSEC_TIME); if (subSecs != null) { try { long sub = Long.valueOf(subSecs); @@ -386,10 +1524,11 @@ public class ExifInterface { } msecs += sub; } catch (NumberFormatException e) { + // Ignored } } return msecs; - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException e) { return -1; } } @@ -400,9 +1539,13 @@ public class ExifInterface { * @hide */ public long getGpsDateTime() { - String date = mAttributes.get(TAG_GPS_DATESTAMP); - String time = mAttributes.get(TAG_GPS_TIMESTAMP); - if (date == null || time == null) return -1; + String date = getAttribute(TAG_GPS_DATESTAMP); + String time = getAttribute(TAG_GPS_TIMESTAMP); + if (date == null || time == null + || (!sNonZeroTimePattern.matcher(date).matches() + && !sNonZeroTimePattern.matcher(time).matches())) { + return -1; + } String dateTimeString = date + ' ' + time; @@ -411,13 +1554,12 @@ public class ExifInterface { Date datetime = sFormatter.parse(dateTimeString, pos); if (datetime == null) return -1; return datetime.getTime(); - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException e) { return -1; } } - private static float convertRationalLatLonToFloat( - String rationalString, String ref) { + private static float convertRationalLatLonToFloat(String rationalString, String ref) { try { String [] parts = rationalString.split(","); @@ -439,26 +1581,949 @@ public class ExifInterface { return (float) -result; } return (float) result; - } catch (NumberFormatException e) { - // Some of the nubmers are not valid - throw new IllegalArgumentException(); - } catch (ArrayIndexOutOfBoundsException e) { - // Some of the rational does not follow the correct format + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + // Not valid throw new IllegalArgumentException(); } } - private 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; + 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)); + } + ++bytesRead; + while (true) { + 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)); + } + ++bytesRead; + + // 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; + } + int length = dataInputStream.readUnsignedShort() - 2; + bytesRead += 2; + if (DEBUG) { + Log.d(TAG, "JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: " + + (length + 2) + ")"); + } + if (length < 0) { + throw new IOException("Invalid length"); + } + switch (marker) { + case MARKER_APP1: { + if (DEBUG) { + Log.d(TAG, "MARKER_APP1"); + } + if (length < 6) { + // Skip if it's not an EXIF APP1 segment. + break; + } + byte[] identifier = new byte[6]; + if (inputStream.read(identifier) != 6) { + throw new IOException("Invalid exif"); + } + bytesRead += 6; + length -= 6; + if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { + // Skip if it's not an EXIF APP1 segment. + break; + } + if (length <= 0) { + throw new IOException("Invalid exif"); + } + if (DEBUG) { + Log.d(TAG, "readExifSegment with a byte array (length: " + length + ")"); + } + byte[] bytes = new byte[length]; + if (dataInputStream.read(bytes) != length) { + throw new IOException("Invalid exif"); + } + readExifSegment(bytes, bytesRead); + bytesRead += length; + length = 0; + break; + } + + case MARKER_COM: { + byte[] bytes = new byte[length]; + if (dataInputStream.read(bytes) != length) { + throw new IOException("Invalid exif"); + } + length = 0; + if (getAttribute(TAG_USER_COMMENT) == null) { + mAttributes[IFD_EXIF_HINT].put(TAG_USER_COMMENT, ExifAttribute.createString( + new String(bytes, ASCII))); + } + break; + } + + 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: { + if (dataInputStream.skipBytes(1) != 1) { + throw new IOException("Invalid SOFx"); + } + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong( + dataInputStream.readUnsignedShort(), mExifByteOrder)); + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong( + dataInputStream.readUnsignedShort(), mExifByteOrder)); + length -= 5; + break; + } + + default: { + break; + } + } + if (length < 0) { + throw new IOException("Invalid length"); + } + if (dataInputStream.skipBytes(length) != length) { + throw new IOException("Invalid JPEG segment"); + } + bytesRead += length; + } + } + + // 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); + ByteOrderAwarenessDataOutputStream dataOutputStream = + new ByteOrderAwarenessDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN); + if (dataInputStream.readByte() != MARKER) { + throw new IOException("Invalid marker"); + } + dataOutputStream.writeByte(MARKER); + if (dataInputStream.readByte() != MARKER_SOI) { + throw new IOException("Invalid marker"); + } + dataOutputStream.writeByte(MARKER_SOI); + + // Write EXIF APP1 segment + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(MARKER_APP1); + writeExifSegment(dataOutputStream, 6); + + byte[] bytes = new byte[4096]; + + while (true) { + byte marker = dataInputStream.readByte(); + if (marker != MARKER) { + throw new IOException("Invalid marker"); + } + marker = dataInputStream.readByte(); + switch (marker) { + case MARKER_APP1: { + int length = dataInputStream.readUnsignedShort() - 2; + if (length < 0) { + throw new IOException("Invalid length"); + } + byte[] identifier = new byte[6]; + if (length >= 6) { + if (dataInputStream.read(identifier) != 6) { + throw new IOException("Invalid exif"); + } + if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { + // Skip the original EXIF APP1 segment. + if (dataInputStream.skip(length - 6) != length - 6) { + throw new IOException("Invalid length"); + } + break; + } + } + // Copy non-EXIF APP1 segment. + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + dataOutputStream.writeUnsignedShort(length + 2); + if (length >= 6) { + length -= 6; + dataOutputStream.write(identifier); + } + int read; + while (length > 0 && (read = dataInputStream.read( + bytes, 0, Math.min(length, bytes.length))) >= 0) { + dataOutputStream.write(bytes, 0, read); + length -= read; + } + break; + } + case MARKER_EOI: + case MARKER_SOS: { + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + // Copy all the remaining data + Streams.copy(dataInputStream, dataOutputStream); + return; + } + default: { + // Copy JPEG segment + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + int length = dataInputStream.readUnsignedShort(); + dataOutputStream.writeUnsignedShort(length); + length -= 2; + if (length < 0) { + throw new IOException("Invalid length"); + } + int read; + while (length > 0 && (read = dataInputStream.read( + bytes, 0, Math.min(length, bytes.length))) >= 0) { + dataOutputStream.write(bytes, 0, read); + length -= read; + } + 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"); + } + mExifByteOrder = ByteOrder.LITTLE_ENDIAN; + break; + case BYTE_ALIGN_MM: + if (DEBUG) { + Log.d(TAG, "readExifSegment: Byte Align MM"); + } + mExifByteOrder = ByteOrder.BIG_ENDIAN; + break; + default: + throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder)); + } + + // Set byte order. + dataInputStream.setByteOrder(mExifByteOrder); + + 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. + String jpegInterchangeFormatString = getAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name); + String jpegInterchangeFormatLengthString = + getAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name); + if (jpegInterchangeFormatString != null && jpegInterchangeFormatLengthString != null) { + try { + int jpegInterchangeFormat = Integer.parseInt(jpegInterchangeFormatString); + int jpegInterchangeFormatLength = Integer + .parseInt(jpegInterchangeFormatLengthString); + // The following code limits the size of thumbnail size not to overflow EXIF data area. + jpegInterchangeFormatLength = Math.min(jpegInterchangeFormat + + jpegInterchangeFormatLength, exifBytes.length) - jpegInterchangeFormat; + if (jpegInterchangeFormat > 0 && jpegInterchangeFormatLength > 0) { + mHasThumbnail = true; + mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat; + mThumbnailLength = jpegInterchangeFormatLength; + } + } catch (NumberFormatException e) { + // Ignored the corrupted image. + } + } + } + + private void addDefaultValuesForCompatibility() { + // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag. + String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL); + if (valueOfDateTimeOriginal != null) { + mAttributes[IFD_TIFF_HINT].put(TAG_DATETIME, + ExifAttribute.createString(valueOfDateTimeOriginal)); + } + + // Add the default value. + if (getAttribute(TAG_IMAGE_WIDTH) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_IMAGE_LENGTH) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_ORIENTATION) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_ORIENTATION, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_LIGHT_SOURCE) == null) { + mAttributes[IFD_EXIF_HINT].put(TAG_LIGHT_SOURCE, + ExifAttribute.createULong(0, mExifByteOrder)); + } + } + + // Reads image file directory, which is a tag group in EXIF. + private void readImageFileDirectory(ByteOrderAwarenessDataInputStream dataInputStream, int hint) + throws IOException { + if (dataInputStream.peek() + 2 > dataInputStream.mLength) { + // Return if there is no data from the offset. + return; + } + // See JEITA CP-3451 Figure 5. page 9. + short numberOfDirectoryEntry = dataInputStream.readShort(); + if (dataInputStream.peek() + 12 * numberOfDirectoryEntry > dataInputStream.mLength) { + // Return if the size of entries is too big. + return; + } + + 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. + // Look up a corresponding tag from tag number + final ExifTag tag = (ExifTag) sExifTagMapsForReading[hint].get(tagNumber); + + if (DEBUG) { + Log.d(TAG, String.format("hint: %d, tagNumber: %d, tagName: %s, dataFormat: %d, " + + "numberOfComponents: %d", hint, tagNumber, tag != null ? tag.name : null, + dataFormat, numberOfComponents)); + } + + if (tag == null || dataFormat <= 0 || + dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) { + // Skip if the parsed tag number is not defined or invalid data format. + if (tag == null) { + Log.w(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber); + } else { + Log.w(TAG, "Skip the tag entry since data format is invalid: " + dataFormat); + } + dataInputStream.seek(nextEntryOffset); + continue; + } + + // 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); + } + if (offset + byteCount <= dataInputStream.mLength) { + dataInputStream.seek(offset); + } else { + // Skip if invalid data offset. + Log.w(TAG, "Skip the tag entry since data offset is invalid: " + offset); + dataInputStream.seek(nextEntryOffset); + continue; + } + } + + // Recursively parse IFD when a IFD pointer tag appears. + int innerIfdHint = getIfdHintFromTagNumber(tagNumber); + if (DEBUG) { + Log.d(TAG, "innerIfdHint: " + innerIfdHint + " byteCount: " + byteCount); + } + + 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, tag.name)); + } + if (offset > 0L && offset < dataInputStream.mLength) { + dataInputStream.seek(offset); + readImageFileDirectory(dataInputStream, innerIfdHint); + } else { + Log.w(TAG, "Skip jump into the IFD since its offset is invalid: " + offset); + } + + dataInputStream.seek(nextEntryOffset); + continue; + } + + byte[] bytes = new byte[numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat]]; + dataInputStream.readFully(bytes); + mAttributes[hint].put( + tag.name, new ExifAttribute(dataFormat, numberOfComponents, bytes)); + if (dataInputStream.peek() != nextEntryOffset) { + dataInputStream.seek(nextEntryOffset); + } + } + + if (dataInputStream.peek() + 4 <= dataInputStream.mLength) { + 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 && nextIfdOffset < dataInputStream.mLength) { + dataInputStream.seek(nextIfdOffset); + readImageFileDirectory(dataInputStream, IFD_THUMBNAIL_HINT); + } + } + } + + // 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(ByteOrderAwarenessDataOutputStream 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]; + + // Remove IFD pointer tags (we'll re-add it later.) + for (ExifTag tag : IFD_POINTER_TAGS) { + removeAttribute(tag.name); + } + // Remove old thumbnail data + removeAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name); + removeAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name); + + // Remove null value tags. + for (int hint = 0; hint < EXIF_TAGS.length; ++hint) { + for (Object obj : mAttributes[hint].entrySet().toArray()) { + final Map.Entry entry = (Map.Entry) obj; + if (entry.getValue() == null) { + mAttributes[hint].remove(entry.getKey()); + } + } + } - private native void saveAttributesNative(String fileName, - String compressedAttributes); + // 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 (!mAttributes[IFD_INTEROPERABILITY_HINT].isEmpty()) { + mAttributes[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (!mAttributes[IFD_EXIF_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (!mAttributes[IFD_GPS_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (mHasThumbnail) { + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + ExifAttribute.createULong(0, mExifByteOrder)); + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name, + ExifAttribute.createULong(mThumbnailLength, mExifByteOrder)); + } - private native String getAttributesNative(String fileName); + // 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 < EXIF_TAGS.length; ++i) { + int sum = 0; + for (Map.Entry entry : (Set) mAttributes[i].entrySet()) { + final ExifAttribute exifAttribute = (ExifAttribute) entry.getValue(); + final int size = exifAttribute.size(); + if (size > 4) { + sum += size; + } + } + ifdDataSizes[i] += sum; + } - private native void commitChangesNative(String fileName); + // Calculate IFD offsets. + int position = 8; + for (int hint = 0; hint < EXIF_TAGS.length; ++hint) { + if (!mAttributes[hint].isEmpty()) { + ifdOffsets[hint] = position; + position += 2 + mAttributes[hint].size() * 12 + 4 + ifdDataSizes[hint]; + } + } + if (mHasThumbnail) { + int thumbnailOffset = position; + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + ExifAttribute.createULong(thumbnailOffset, mExifByteOrder)); + mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset; + position += mThumbnailLength; + } - private native byte[] getThumbnailNative(String fileName); + // 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 < EXIF_TAGS.length; ++i) { + Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d", + i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i])); + } + } - private native long[] getThumbnailRangeNative(String fileName); + // Update IFD pointer tags with the calculated offsets. + if (!mAttributes[IFD_EXIF_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].name, + ExifAttribute.createULong(ifdOffsets[IFD_EXIF_HINT], mExifByteOrder)); + } + if (!mAttributes[IFD_GPS_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].name, + ExifAttribute.createULong(ifdOffsets[IFD_GPS_HINT], mExifByteOrder)); + } + if (!mAttributes[IFD_INTEROPERABILITY_HINT].isEmpty()) { + mAttributes[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].name, ExifAttribute.createULong( + ifdOffsets[IFD_INTEROPERABILITY_HINT], mExifByteOrder)); + } + + // Write TIFF Headers. See JEITA CP-3451C Table 1. page 10. + dataOutputStream.writeUnsignedShort(totalSize); + dataOutputStream.write(IDENTIFIER_EXIF_APP1); + dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN + ? BYTE_ALIGN_MM : BYTE_ALIGN_II); + dataOutputStream.setByteOrder(mExifByteOrder); + 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 (!mAttributes[hint].isEmpty()) { + // See JEITA CP-3451C 4.6.2 IFD structure. page 13. + // Write entry count + dataOutputStream.writeUnsignedShort(mAttributes[hint].size()); + + // Write entry info + int dataOffset = ifdOffsets[hint] + 2 + mAttributes[hint].size() * 12 + 4; + for (Map.Entry entry : (Set) mAttributes[hint].entrySet()) { + // Convert tag name to tag number. + final ExifTag tag = (ExifTag) sExifTagMapsForWriting[hint].get(entry.getKey()); + final int tagNumber = tag.number; + final ExifAttribute attribute = (ExifAttribute) entry.getValue(); + final int size = attribute.size(); + + dataOutputStream.writeUnsignedShort(tagNumber); + dataOutputStream.writeUnsignedShort(attribute.format); + dataOutputStream.writeInt(attribute.numberOfComponents); + if (size > 4) { + dataOutputStream.writeUnsignedInt(dataOffset); + dataOffset += size; + } else { + dataOutputStream.write(attribute.bytes); + // Fill zero up to 4 bytes + if (size < 4) { + for (int i = size; i < 4; ++i) { + dataOutputStream.writeByte(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 && !mAttributes[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 (Map.Entry entry : (Set) mAttributes[hint].entrySet()) { + ExifAttribute attribute = (ExifAttribute) entry.getValue(); + + if (attribute.bytes.length > 4) { + dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length); + } + } + } + } + + // Write thumbnail + if (mHasThumbnail) { + dataOutputStream.write(getThumbnail()); + } + + // Reset the byte order to big endian in order to write remaining parts of the JPEG file. + dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); + + return totalSize; + } + + /** + * Determines the data format of EXIF entry value. + * + * @param entryValue The value to be determined. + * @return Returns two data formats gussed as a pair in integer. If there is no two candidate + data formats for the given entry value, returns {@code -1} in the second of the pair. + */ + private static Pair guessDataFormat(String entryValue) { + // See TIFF 6.0 spec Types. page 15. + // Take the first component if there are more than one component. + if (entryValue.contains(",")) { + String[] entryValues = entryValue.split(","); + Pair dataFormat = guessDataFormat(entryValues[0]); + if (dataFormat.first == IFD_FORMAT_STRING) { + return dataFormat; + } + for (int i = 1; i < entryValues.length; ++i) { + final Pair guessDataFormat = guessDataFormat(entryValues[i]); + int first = -1, second = -1; + if (guessDataFormat.first == dataFormat.first + || guessDataFormat.second == dataFormat.first) { + first = dataFormat.first; + } + if (dataFormat.second != -1 && (guessDataFormat.first == dataFormat.second + || guessDataFormat.second == dataFormat.second)) { + second = dataFormat.second; + } + if (first == -1 && second == -1) { + return new Pair<>(IFD_FORMAT_STRING, -1); + } + if (first == -1) { + dataFormat = new Pair<>(second, -1); + continue; + } + if (second == -1) { + dataFormat = new Pair<>(first, -1); + continue; + } + } + return dataFormat; + } + + if (entryValue.contains("/")) { + String[] rationalNumber = entryValue.split("/"); + if (rationalNumber.length == 2) { + try { + long numerator = Long.parseLong(rationalNumber[0]); + long denominator = Long.parseLong(rationalNumber[1]); + if (numerator < 0L || denominator < 0L) { + return new Pair<>(IFD_FORMAT_SRATIONAL, - 1); + } + if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) { + return new Pair<>(IFD_FORMAT_URATIONAL, -1); + } + return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL); + } catch (NumberFormatException e) { + // Ignored + } + } + return new Pair<>(IFD_FORMAT_STRING, -1); + } + try { + Long longValue = Long.parseLong(entryValue); + if (longValue >= 0 && longValue <= 65535) { + return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG); + } + if (longValue < 0) { + return new Pair<>(IFD_FORMAT_SLONG, -1); + } + return new Pair<>(IFD_FORMAT_ULONG, -1); + } catch (NumberFormatException e) { + // Ignored + } + try { + Double.parseDouble(entryValue); + return new Pair<>(IFD_FORMAT_DOUBLE, -1); + } catch (NumberFormatException e) { + // Ignored + } + return new Pair<>(IFD_FORMAT_STRING, -1); + } + + // 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) + 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) + 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, which can be written in either little or big endian + // order. + private static class ByteOrderAwarenessDataOutputStream extends FilterOutputStream { + private final OutputStream mOutputStream; + private ByteOrder mByteOrder; + + public ByteOrderAwarenessDataOutputStream(OutputStream out, ByteOrder byteOrder) { + super(out); + mOutputStream = out; + mByteOrder = byteOrder; + } + + public void setByteOrder(ByteOrder byteOrder) { + mByteOrder = byteOrder; + } + + public void write(byte[] bytes) throws IOException { + mOutputStream.write(bytes); + } + + public void write(byte[] bytes, int offset, int length) throws IOException { + mOutputStream.write(bytes, offset, length); + } + + public void writeByte(int val) throws IOException { + mOutputStream.write(val); + } + + public void writeShort(short val) throws IOException { + if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + mOutputStream.write((val >>> 0) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 0) & 0xFF); + } + } + + public void writeInt(int val) throws IOException { + if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + mOutputStream.write((val >>> 0) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 16) & 0xFF); + mOutputStream.write((val >>> 24) & 0xFF); + } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + mOutputStream.write((val >>> 24) & 0xFF); + mOutputStream.write((val >>> 16) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 0) & 0xFF); + } + } + + public void writeUnsignedShort(int val) throws IOException { + writeShort((short) val); + } + + public void writeUnsignedInt(long val) throws IOException { + writeInt((int) val); + } + } } diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 79557bc..241df15 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -42,13 +42,9 @@ LOCAL_SHARED_LIBRARIES := \ libcamera_client \ libmtp \ libusbhost \ - libjhead \ libexif \ libstagefright_amrnb_common -LOCAL_REQUIRED_MODULES := \ - libjhead_jni - LOCAL_STATIC_LIBRARIES := \ libstagefright_amrnbenc -- cgit v1.1 From d473effc7f81cef2a90d77c18c3fe33bbeb58c62 Mon Sep 17 00:00:00 2001 From: Sungsoo Lim Date: Mon, 25 Jul 2016 11:53:13 +0900 Subject: DO NOT MERGE: Fix CTS regression Bug: 30297223, Bug: 30437363 Change-Id: I7b18af40e4eac2713577204428fbfb96cc346582 --- media/java/android/media/ExifInterface.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index c5e978f..74bb55b 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -1330,12 +1330,12 @@ public class ExifInterface { * determine whether the image data format is JPEG or not. */ private void loadAttributes() throws IOException { + // Initialize mAttributes. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + mAttributes[i] = new HashMap(); + } try { InputStream in = new FileInputStream(mFilename); - // Initialize mAttributes. - for (int i = 0; i < EXIF_TAGS.length; ++i) { - mAttributes[i] = new HashMap(); - } getJpegAttributes(in); } catch (IOException e) { // Ignore exceptions in order to keep the compatibility with the old versions of -- cgit v1.1 From 43b5aa6bd6263e8ea87bda8f718eb3b080eed62b Mon Sep 17 00:00:00 2001 From: Sudheer Shanka Date: Fri, 29 Jul 2016 10:50:10 -0700 Subject: DO NOT MERGE: Allow apps with CREATE_USERS permission to call UM.getProfiles. Bug: 29189712 Bug: 30317026 Bug: 30235113 Change-Id: Icced9805a56675e86f894c458c4a5a0048fd54c0 --- services/core/java/com/android/server/pm/UserManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index fd9979f..c5669cc 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -296,7 +296,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public List getProfiles(int userId, boolean enabledOnly) { if (userId != UserHandle.getCallingUserId()) { - checkManageUsersPermission("getting profiles related to user " + userId); + checkManageOrCreateUsersPermission("getting profiles related to user " + userId); } final long ident = Binder.clearCallingIdentity(); try { -- cgit v1.1 From 5a110cf4720b74526880f78056bff863b3954c19 Mon Sep 17 00:00:00 2001 From: Sudheer Shanka Date: Wed, 8 Jun 2016 17:13:24 -0700 Subject: Reduce shell power over user management. Remove MANAGE_USERS permission from shell and whitelist it for some specific functionality. Bug: 29189712 Change-Id: Ifb37448c091af91991964511e3efb1bb4dea1ff3 --- core/res/AndroidManifest.xml | 8 +++ packages/Shell/AndroidManifest.xml | 2 +- .../com/android/server/pm/UserManagerService.java | 80 ++++++++++++++++++++-- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 08bccc1..4b54459 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1392,6 +1392,14 @@ + + + - + diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c5669cc..583d764 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -125,6 +125,11 @@ public class UserManagerService extends IUserManager.Stub { private static final String RESTRICTIONS_FILE_PREFIX = "res_"; private static final String XML_SUFFIX = ".xml"; + private static final int ALLOWED_FLAGS_FOR_CREATE_USERS_PERMISSION = + UserInfo.FLAG_MANAGED_PROFILE + | UserInfo.FLAG_RESTRICTED + | UserInfo.FLAG_GUEST; + private static final int MIN_USER_ID = 10; private static final int USER_VERSION = 5; @@ -277,7 +282,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public List getUsers(boolean excludeDying) { - checkManageUsersPermission("query users"); + checkManageOrCreateUsersPermission("query users"); synchronized (mPackagesLock) { ArrayList users = new ArrayList(mUsers.size()); for (int i = 0; i < mUsers.size(); i++) { @@ -388,7 +393,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public UserInfo getUserInfo(int userId) { - checkManageUsersPermission("query user"); + checkManageOrCreateUsersPermission("query user"); synchronized (mPackagesLock) { return getUserInfoLocked(userId); } @@ -676,6 +681,71 @@ public class UserManagerService extends IUserManager.Stub { } } + /** + * Enforces that only the system UID or root's UID or apps that have the + * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or + * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS} + * can make certain calls to the UserManager. + * + * @param message used as message if SecurityException is thrown + * @throws SecurityException if the caller is not system or root + * @see #hasManageOrCreateUsersPermission() + */ + private static final void checkManageOrCreateUsersPermission(String message) { + if (!hasManageOrCreateUsersPermission()) { + throw new SecurityException( + "You either need MANAGE_USERS or CREATE_USERS permission to: " + message); + } + } + + /** + * Similar to {@link #checkManageOrCreateUsersPermission(String)} but when the caller is tries + * to create user/profiles other than what is allowed for + * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS} permission, then it will only + * allow callers with {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} permission. + */ + private static final void checkManageOrCreateUsersPermission(int creationFlags) { + if ((creationFlags & ~ALLOWED_FLAGS_FOR_CREATE_USERS_PERMISSION) == 0) { + if (!hasManageOrCreateUsersPermission()) { + throw new SecurityException("You either need MANAGE_USERS or CREATE_USERS " + + "permission to create an user with flags: " + creationFlags); + } + } else if (!hasManageUsersPermission()) { + throw new SecurityException("You need MANAGE_USERS permission to create an user " + + " with flags: " + creationFlags); + } + } + + /** + * @return whether the calling UID is system UID or root's UID or the calling app has the + * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS}. + */ + private static final boolean hasManageUsersPermission() { + final int callingUid = Binder.getCallingUid(); + return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID) + || callingUid == Process.ROOT_UID + || ActivityManager.checkComponentPermission( + android.Manifest.permission.MANAGE_USERS, + callingUid, -1, true) == PackageManager.PERMISSION_GRANTED; + } + + /** + * @return whether the calling UID is system UID or root's UID or the calling app has the + * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or + * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS}. + */ + private static final boolean hasManageOrCreateUsersPermission() { + final int callingUid = Binder.getCallingUid(); + return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID) + || callingUid == Process.ROOT_UID + || ActivityManager.checkComponentPermission( + android.Manifest.permission.MANAGE_USERS, + callingUid, -1, true) == PackageManager.PERMISSION_GRANTED + || ActivityManager.checkComponentPermission( + android.Manifest.permission.CREATE_USERS, + callingUid, -1, true) == PackageManager.PERMISSION_GRANTED; + } + private static void checkSystemOrRoot(String message) { final int uid = Binder.getCallingUid(); if (uid != Process.SYSTEM_UID && uid != 0) { @@ -1227,7 +1297,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public UserInfo createProfileForUser(String name, int flags, int userId) { - checkManageUsersPermission("Only the system can create users"); + checkManageOrCreateUsersPermission(flags); if (userId != UserHandle.USER_OWNER) { Slog.w(LOG_TAG, "Only user owner can have profiles"); return null; @@ -1237,7 +1307,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public UserInfo createUser(String name, int flags) { - checkManageUsersPermission("Only the system can create users"); + checkManageOrCreateUsersPermission(flags); return createUserInternal(name, flags, UserHandle.USER_NULL); } @@ -1402,7 +1472,7 @@ public class UserManagerService extends IUserManager.Stub { * @param userHandle the user's id */ public boolean removeUser(int userHandle) { - checkManageUsersPermission("Only the system can remove users"); + checkManageOrCreateUsersPermission("Only the system can remove users"); if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean( UserManager.DISALLOW_REMOVE_USER, false)) { Log.w(LOG_TAG, "Cannot remove user. DISALLOW_REMOVE_USER is enabled."); -- cgit v1.1 From 00fad46e542f8d358ffc17bab9ce042687d85623 Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Fri, 5 Aug 2016 15:25:03 -0700 Subject: DO NOT MERGE: Clean up when recycling a pid with a pending launch Fix for accidental launch of a broadcast receiver in an incorrect app instance. Bug: 30202481 Change-Id: I8ec8f19c633f3aec8da084dab5fd5b312443336f (cherry picked from commit d1eeb5b7b489d47994a71510f1ed5b97b8e32a7a) --- .../android/server/am/ActivityManagerService.java | 21 +++++++++++++++++---- .../java/com/android/server/am/BroadcastQueue.java | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 33d0a9f..9ff8415 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -3424,6 +3424,15 @@ public final class ActivityManagerService extends ActivityManagerNative app.killedByAm = false; checkTime(startTime, "startProcess: starting to update pids map"); synchronized (mPidsSelfLocked) { + ProcessRecord oldApp; + // If there is already an app occupying that pid that hasn't been cleaned up + if ((oldApp = mPidsSelfLocked.get(startResult.pid)) != null && !app.isolated) { + // Clean up anything relating to this pid first + Slog.w(TAG, "Reusing pid " + startResult.pid + + " while app is still mapped to it"); + cleanUpApplicationRecordLocked(oldApp, false, false, -1, + true /*replacingPid*/); + } this.mPidsSelfLocked.put(startResult.pid, app); if (isActivityProcess) { Message msg = mHandler.obtainMessage(PROC_START_TIMEOUT_MSG); @@ -4564,7 +4573,8 @@ public final class ActivityManagerService extends ActivityManagerNative private final void handleAppDiedLocked(ProcessRecord app, boolean restarting, boolean allowRestart) { int pid = app.pid; - boolean kept = cleanUpApplicationRecordLocked(app, restarting, allowRestart, -1); + boolean kept = cleanUpApplicationRecordLocked(app, restarting, allowRestart, -1, + false /*replacingPid*/); if (!kept && !restarting) { removeLruProcessLocked(app); if (pid > 0) { @@ -15502,7 +15512,8 @@ public final class ActivityManagerService extends ActivityManagerNative * app that was passed in must remain on the process lists. */ private final boolean cleanUpApplicationRecordLocked(ProcessRecord app, - boolean restarting, boolean allowRestart, int index) { + boolean restarting, boolean allowRestart, int index, boolean replacingPid) { + Slog.d(TAG, "cleanUpApplicationRecord -- " + app.pid); if (index >= 0) { removeLruProcessLocked(app); ProcessList.remove(app.pid); @@ -15632,7 +15643,9 @@ public final class ActivityManagerService extends ActivityManagerNative if (!app.persistent || app.isolated) { if (DEBUG_PROCESSES || DEBUG_CLEANUP) Slog.v(TAG_CLEANUP, "Removing non-persistent process during cleanup: " + app); - removeProcessNameLocked(app.processName, app.uid); + if (!replacingPid) { + removeProcessNameLocked(app.processName, app.uid); + } if (mHeavyWeightProcess == app) { mHandler.sendMessage(mHandler.obtainMessage(CANCEL_HEAVY_NOTIFICATION_MSG, mHeavyWeightProcess.userId, 0)); @@ -19473,7 +19486,7 @@ public final class ActivityManagerService extends ActivityManagerNative // Ignore exceptions. } } - cleanUpApplicationRecordLocked(app, false, true, -1); + cleanUpApplicationRecordLocked(app, false, true, -1, false /*replacingPid*/); mRemovedProcesses.remove(i); if (app.persistent) { diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 6de8579..089c33b 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -292,6 +292,11 @@ public final class BroadcastQueue { boolean didSomething = false; final BroadcastRecord br = mPendingBroadcast; if (br != null && br.curApp.pid == app.pid) { + if (br.curApp != app) { + Slog.e(TAG, "App mismatch when sending pending broadcast to " + + app.processName + ", intended target is " + br.curApp.processName); + return false; + } try { mPendingBroadcast = null; processCurBroadcastLocked(br, app); -- cgit v1.1 From ae3aef22e808bdc87ae7104ab4ae878ada9ed2ef Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 10 Aug 2016 15:43:17 -0700 Subject: Fix vulnerability in LockSettings service Fixes bug 30003944 Change-Id: I8700d4424c6186c8d5e71d2fdede0223ad86904d (cherry picked from commit 2d71384a139ae27cbc7b57f06662bf6ee2010f2b) --- core/java/com/android/internal/widget/LockPatternUtils.java | 4 ++-- services/core/java/com/android/server/LockSettingsService.java | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 60380fb..0aef320 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -291,7 +291,7 @@ public class LockPatternUtils { return false; } } catch (RemoteException re) { - return true; + return false; } } @@ -340,7 +340,7 @@ public class LockPatternUtils { return false; } } catch (RemoteException re) { - return true; + return false; } } diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java index 55682c2..6cb2875 100644 --- a/services/core/java/com/android/server/LockSettingsService.java +++ b/services/core/java/com/android/server/LockSettingsService.java @@ -519,6 +519,9 @@ public class LockSettingsService extends ILockSettings.Stub { private VerifyCredentialResponse doVerifyPattern(String pattern, boolean hasChallenge, long challenge, int userId) throws RemoteException { checkPasswordReadPermission(userId); + if (TextUtils.isEmpty(pattern)) { + throw new IllegalArgumentException("Pattern can't be null or empty"); + } CredentialHash storedHash = mStorage.readPatternHash(userId); boolean shouldReEnrollBaseZero = storedHash != null && storedHash.isBaseZeroPattern; @@ -575,6 +578,9 @@ public class LockSettingsService extends ILockSettings.Stub { private VerifyCredentialResponse doVerifyPassword(String password, boolean hasChallenge, long challenge, int userId) throws RemoteException { checkPasswordReadPermission(userId); + if (TextUtils.isEmpty(password)) { + throw new IllegalArgumentException("Password can't be null or empty"); + } CredentialHash storedHash = mStorage.readPasswordHash(userId); return verifyCredential(userId, storedHash, password, hasChallenge, challenge, new CredentialUtil() { -- cgit v1.1 From a08dfe9f35f032bd015e05ca305dc58c9c10651d Mon Sep 17 00:00:00 2001 From: Narayan Kamath Date: Tue, 9 Aug 2016 17:00:25 +0100 Subject: Process: Fix communication with zygote. Don't write partial requests, and don't return (or throw) early after partially reading a response. bug: 30143607 (cherry-picked from commit 448be0a62209c977593d81617853a8a428d013df) Change-Id: I5881fdd5e81023cd21fb4d23a471a5031987a1f1 (cherry picked from commit e29c6493c07acf1a0b706917e9af0c8d761c8ae9) --- core/java/android/os/Process.java | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 7234e98..e0a0fd0 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -538,6 +538,15 @@ public class Process { ZygoteState zygoteState, ArrayList args) throws ZygoteStartFailedEx { try { + // Throw early if any of the arguments are malformed. This means we can + // avoid writing a partial response to the zygote. + int sz = args.size(); + for (int i = 0; i < sz; i++) { + if (args.get(i).indexOf('\n') >= 0) { + throw new ZygoteStartFailedEx("embedded newlines not allowed"); + } + } + /** * See com.android.internal.os.ZygoteInit.readArgumentList() * Presently the wire format to the zygote process is: @@ -554,13 +563,8 @@ public class Process { writer.write(Integer.toString(args.size())); writer.newLine(); - int sz = args.size(); for (int i = 0; i < sz; i++) { String arg = args.get(i); - if (arg.indexOf('\n') >= 0) { - throw new ZygoteStartFailedEx( - "embedded newlines not allowed"); - } writer.write(arg); writer.newLine(); } @@ -569,11 +573,16 @@ public class Process { // Should there be a timeout on this? ProcessStartResult result = new ProcessStartResult(); + + // Always read the entire result from the input stream to avoid leaving + // bytes in the stream for future process starts to accidentally stumble + // upon. result.pid = inputStream.readInt(); + result.usingWrapper = inputStream.readBoolean(); + if (result.pid < 0) { throw new ZygoteStartFailedEx("fork() failed"); } - result.usingWrapper = inputStream.readBoolean(); return result; } catch (IOException ex) { zygoteState.close(); -- cgit v1.1 From 3d2b855e53776b4406e1fb01f6198be89c9f8114 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Thu, 18 Aug 2016 20:22:33 -0700 Subject: Bind fingerprint when we start authentication - DO NOT MERGE This fixes a bug where it was possible to authenticate the wrong user. We now bind the userId when we start authentication and confirm it when authentication completes. Fixes bug 30744668 Change-Id: I346d92c301414ed81e11fa9c171584c7ae4341c2 (cherry picked from commit b6f4b48df273d210d13631b4c2426482feb40c97) --- .../hardware/fingerprint/FingerprintManager.java | 21 ++++++++++++------ .../fingerprint/IFingerprintServiceReceiver.aidl | 2 +- .../android/keyguard/KeyguardUpdateMonitor.java | 9 ++++++-- .../server/fingerprint/FingerprintService.java | 25 ++++++++++++++-------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 122df23..62396a3 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -258,6 +258,7 @@ public class FingerprintManager { public static class AuthenticationResult { private Fingerprint mFingerprint; private CryptoObject mCryptoObject; + private int mUserId; /** * Authentication result @@ -266,9 +267,10 @@ public class FingerprintManager { * @param fingerprint the recognized fingerprint data, if allowed. * @hide */ - public AuthenticationResult(CryptoObject crypto, Fingerprint fingerprint) { + public AuthenticationResult(CryptoObject crypto, Fingerprint fingerprint, int userId) { mCryptoObject = crypto; mFingerprint = fingerprint; + mUserId = userId; } /** @@ -285,6 +287,12 @@ public class FingerprintManager { * @hide */ public Fingerprint getFingerprint() { return mFingerprint; } + + /** + * Obtain the userId for which this fingerprint was authenticated. + * @hide + */ + public int getUserId() { return mUserId; } }; /** @@ -754,7 +762,7 @@ public class FingerprintManager { sendAcquiredResult((Long) msg.obj /* deviceId */, msg.arg1 /* acquire info */); break; case MSG_AUTHENTICATION_SUCCEEDED: - sendAuthenticatedSucceeded((Fingerprint) msg.obj); + sendAuthenticatedSucceeded((Fingerprint) msg.obj, msg.arg1 /* userId */); break; case MSG_AUTHENTICATION_FAILED: sendAuthenticatedFailed(); @@ -799,9 +807,10 @@ public class FingerprintManager { } } - private void sendAuthenticatedSucceeded(Fingerprint fp) { + private void sendAuthenticatedSucceeded(Fingerprint fp, int userId) { if (mAuthenticationCallback != null) { - final AuthenticationResult result = new AuthenticationResult(mCryptoObject, fp); + final AuthenticationResult result = + new AuthenticationResult(mCryptoObject, fp, userId); mAuthenticationCallback.onAuthenticationSucceeded(result); } } @@ -941,8 +950,8 @@ public class FingerprintManager { } @Override // binder call - public void onAuthenticationSucceeded(long deviceId, Fingerprint fp) { - mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, fp).sendToTarget(); + public void onAuthenticationSucceeded(long deviceId, Fingerprint fp, int userId) { + mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget(); } @Override // binder call diff --git a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl index 57a429f..b024b29 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl @@ -26,7 +26,7 @@ import android.os.UserHandle; oneway interface IFingerprintServiceReceiver { void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining); void onAcquired(long deviceId, int acquiredInfo); - void onAuthenticationSucceeded(long deviceId, in Fingerprint fp); + void onAuthenticationSucceeded(long deviceId, in Fingerprint fp, int userId); void onAuthenticationFailed(long deviceId); void onError(long deviceId, int error); void onRemoved(long deviceId, int fingerId, int groupId); diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java index 57ee319..f31df51 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -430,7 +430,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { } } - private void handleFingerprintAuthenticated() { + + private void handleFingerprintAuthenticated(int authUserId) { try { final int userId; try { @@ -439,6 +440,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { Log.e(TAG, "Failed to get current user id: ", e); return; } + if (userId != authUserId) { + Log.d(TAG, "Fingerprint authenticated for wrong user: " + authUserId); + return; + } if (isFingerprintDisabled(userId)) { Log.d(TAG, "Fingerprint disabled by DPM for userId: " + userId); return; @@ -705,7 +710,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { @Override public void onAuthenticationSucceeded(AuthenticationResult result) { - handleFingerprintAuthenticated(); + handleFingerprintAuthenticated(result.getUserId()); } @Override diff --git a/services/core/java/com/android/server/fingerprint/FingerprintService.java b/services/core/java/com/android/server/fingerprint/FingerprintService.java index 103ed0a..84aa2d7 100644 --- a/services/core/java/com/android/server/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/fingerprint/FingerprintService.java @@ -127,6 +127,7 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe private IFingerprintDaemon mDaemon; private final PowerManager mPowerManager; private final AlarmManager mAlarmManager; + private int mCurrentUserId = UserHandle.USER_NULL; private final BroadcastReceiver mLockoutReceiver = new BroadcastReceiver() { @Override @@ -337,7 +338,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe return; } stopPendingOperations(true); - mEnrollClient = new ClientMonitor(token, receiver, groupId, restricted, token.toString()); + mEnrollClient = new ClientMonitor(token, receiver, mCurrentUserId, groupId, restricted, + token.toString()); final int timeout = (int) (ENROLLMENT_TIMEOUT_MS / MS_PER_SEC); try { final int result = daemon.enroll(cryptoToken, groupId, timeout); @@ -425,7 +427,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe return; } stopPendingOperations(true); - mAuthClient = new ClientMonitor(token, receiver, groupId, restricted, opPackageName); + mAuthClient = new ClientMonitor(token, receiver, mCurrentUserId, groupId, restricted, + opPackageName); if (inLockoutMode()) { Slog.v(TAG, "In lockout mode; disallowing authentication"); if (!mAuthClient.sendError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)) { @@ -482,7 +485,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe } stopPendingOperations(true); - mRemoveClient = new ClientMonitor(token, receiver, userId, restricted, token.toString()); + mRemoveClient = new ClientMonitor(token, receiver, mCurrentUserId, userId, restricted, + token.toString()); // The fingerprint template ids will be removed when we get confirmation from the HAL try { final int result = daemon.remove(fingerId, userId); @@ -605,15 +609,17 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe private class ClientMonitor implements IBinder.DeathRecipient { IBinder token; IFingerprintServiceReceiver receiver; - int userId; + int userId; // userId of the caller + int currentUserId; // current user id when this was created boolean restricted; // True if client does not have MANAGE_FINGERPRINT permission String owner; - public ClientMonitor(IBinder token, IFingerprintServiceReceiver receiver, int userId, - boolean restricted, String owner) { + public ClientMonitor(IBinder token, IFingerprintServiceReceiver receiver, + int currentUserId, int userId, boolean restricted, String owner) { this.token = token; this.receiver = receiver; this.userId = userId; + this.currentUserId = currentUserId; this.restricted = restricted; this.owner = owner; // name of the client that owns this - for debugging try { @@ -702,9 +708,9 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe Slog.v(TAG, "onAuthenticated(owner=" + mAuthClient.owner + ", id=" + fpId + ", gp=" + groupId + ")"); } - Fingerprint fp = !restricted ? - new Fingerprint("" /* TODO */, groupId, fpId, mHalDeviceId) : null; - receiver.onAuthenticationSucceeded(mHalDeviceId, fp); + Fingerprint fp = !restricted ? new Fingerprint("" /* TODO */, groupId, fpId, + mHalDeviceId) : null; + receiver.onAuthenticationSucceeded(mHalDeviceId, fp, currentUserId); } } catch (RemoteException e) { Slog.w(TAG, "Failed to notify Authenticated:", e); @@ -1129,6 +1135,7 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe Slog.e(TAG, "Failed to setActiveGroup():", e); } } + mCurrentUserId = userId; } private void listenForUserSwitches() { -- cgit v1.1 From 2dde02ed263192cc71f7f11f120b4bf03432f508 Mon Sep 17 00:00:00 2001 From: David Christie Date: Tue, 23 Aug 2016 16:19:51 -0700 Subject: DO NOT MERGE: Fix vulnerability where large GPS XTRA data can be injected. -Can potentially crash system with OOM. Bug: 29555864 Change-Id: I7157f48dddf148a9bcab029cf12e26a58d8054f4 (cherry picked from commit 5439aabb165b5a760d1e580016bf1d6fd963cb65) --- .../android/server/location/GpsXtraDownloader.java | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/location/GpsXtraDownloader.java b/services/core/java/com/android/server/location/GpsXtraDownloader.java index 3585049..6310361 100644 --- a/services/core/java/com/android/server/location/GpsXtraDownloader.java +++ b/services/core/java/com/android/server/location/GpsXtraDownloader.java @@ -21,8 +21,11 @@ import android.util.Log; import java.net.HttpURLConnection; import java.net.URL; -import libcore.io.Streams; +import libcore.io.IoUtils; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.IOException; import java.util.Properties; import java.util.Random; @@ -36,6 +39,7 @@ public class GpsXtraDownloader { private static final String TAG = "GpsXtraDownloader"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final long MAXIMUM_CONTENT_LENGTH_BYTES = 1000000; // 1MB. private static final String DEFAULT_USER_AGENT = "Android"; private final String[] mXtraServers; @@ -121,7 +125,19 @@ public class GpsXtraDownloader { return null; } - return Streams.readFully(connection.getInputStream()); + try (InputStream in = connection.getInputStream()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + if (bytes.size() > MAXIMUM_CONTENT_LENGTH_BYTES) { + if (DEBUG) Log.d(TAG, "XTRA file too large"); + return null; + } + } + return bytes.toByteArray(); + } } catch (IOException ioe) { if (DEBUG) Log.d(TAG, "Error downloading gps XTRA: ", ioe); } finally { @@ -133,3 +149,4 @@ public class GpsXtraDownloader { } } + -- cgit v1.1