summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/callcomposer/camera
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/dialer/callcomposer/camera
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/dialer/callcomposer/camera')
-rw-r--r--java/com/android/dialer/callcomposer/camera/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/callcomposer/camera/CameraManager.java822
-rw-r--r--java/com/android/dialer/callcomposer/camera/CameraPreview.java177
-rw-r--r--java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java125
-rw-r--r--java/com/android/dialer/callcomposer/camera/ImagePersistTask.java143
-rw-r--r--java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java120
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java28
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java482
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java97
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java179
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java816
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java153
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml26
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java129
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifData.java89
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java374
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java24
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifParser.java846
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifReader.java81
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifTag.java619
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/IfdData.java126
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/IfdId.java28
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java38
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/Rational.java70
25 files changed, 5624 insertions, 0 deletions
diff --git a/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
new file mode 100644
index 000000000..82f141284
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.callcomposer.camera"/> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/CameraManager.java b/java/com/android/dialer/callcomposer/camera/CameraManager.java
new file mode 100644
index 000000000..87cd16a99
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraManager.java
@@ -0,0 +1,822 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import com.android.dialer.callcomposer.camera.camerafocus.FocusOverlayManager;
+import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Class which manages interactions with the camera, but does not do any UI. This class is designed
+ * to be a singleton to ensure there is one component managing the camera and releasing the native
+ * resources. In order to acquire a camera, a caller must:
+ *
+ * <ul>
+ * <li>Call selectCamera to select front or back camera
+ * <li>Call setSurface to control where the preview is shown
+ * <li>Call openCamera to request the camera start preview
+ * </ul>
+ *
+ * Callers should call onPause and onResume to ensure that the camera is release while the activity
+ * is not active. This class is not thread safe. It should only be called from one thread (the UI
+ * thread or test thread)
+ */
+public class CameraManager implements FocusOverlayManager.Listener {
+ /** Callbacks for the camera manager listener */
+ public interface CameraManagerListener {
+ void onCameraError(int errorCode, Exception e);
+
+ void onCameraChanged();
+ }
+
+ /** Callback when taking image or video */
+ public interface MediaCallback {
+ int MEDIA_CAMERA_CHANGED = 1;
+ int MEDIA_NO_DATA = 2;
+
+ void onMediaReady(Uri uriToMedia, String contentType, int width, int height);
+
+ void onMediaFailed(Exception exception);
+
+ void onMediaInfo(int what);
+ }
+
+ // Error codes
+ private static final int ERROR_OPENING_CAMERA = 1;
+ private static final int ERROR_SHOWING_PREVIEW = 2;
+ private static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 3;
+ private static final int ERROR_TAKING_PICTURE = 4;
+
+ private static final int NO_CAMERA_SELECTED = -1;
+
+ private static final Camera.ShutterCallback DUMMY_SHUTTER_CALLBACK =
+ new Camera.ShutterCallback() {
+ @Override
+ public void onShutter() {
+ // Do nothing
+ }
+ };
+
+ private static CameraManager sInstance;
+
+ /** The CameraInfo for the currently selected camera */
+ private final CameraInfo mCameraInfo;
+
+ /** The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet */
+ private int mCameraIndex;
+
+ /** True if the device has front and back cameras */
+ private final boolean mHasFrontAndBackCamera;
+
+ /** True if the camera should be open (may not yet be actually open) */
+ private boolean mOpenRequested;
+
+ /** The preview view to show the preview on */
+ private CameraPreview mCameraPreview;
+
+ /** The helper classs to handle orientation changes */
+ private OrientationHandler mOrientationHandler;
+
+ /** Tracks whether the preview has hardware acceleration */
+ private boolean mIsHardwareAccelerationSupported;
+
+ /**
+ * The task for opening the camera, so it doesn't block the UI thread Using AsyncTask rather than
+ * SafeAsyncTask because the tasks need to be serialized, but don't need to be on the UI thread
+ * TODO: If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may need
+ * to create a dedicated thread, or synchronize the threads in the thread pool
+ */
+ private AsyncTask<Integer, Void, Camera> mOpenCameraTask;
+
+ /**
+ * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if
+ * no open task is pending
+ */
+ private int mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+
+ /** The instance of the currently opened camera */
+ private Camera mCamera;
+
+ /** The rotation of the screen relative to the camera's natural orientation */
+ private int mRotation;
+
+ /** The callback to notify when errors or other events occur */
+ private CameraManagerListener mListener;
+
+ /** True if the camera is currently in the process of taking an image */
+ private boolean mTakingPicture;
+
+ /** Manages auto focus visual and behavior */
+ private final FocusOverlayManager mFocusOverlayManager;
+
+ private CameraManager() {
+ mCameraInfo = new CameraInfo();
+ mCameraIndex = NO_CAMERA_SELECTED;
+
+ // Check to see if a front and back camera exist
+ boolean hasFrontCamera = false;
+ boolean hasBackCamera = false;
+ final CameraInfo cameraInfo = new CameraInfo();
+ final int cameraCount = Camera.getNumberOfCameras();
+ try {
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
+ hasFrontCamera = true;
+ } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
+ hasBackCamera = true;
+ }
+ if (hasFrontCamera && hasBackCamera) {
+ break;
+ }
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.CameraManager", "Unable to load camera info", e);
+ }
+ mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera;
+ mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper());
+
+ // Assume the best until we are proven otherwise
+ mIsHardwareAccelerationSupported = true;
+ }
+
+ /** Gets the singleton instance */
+ public static CameraManager get() {
+ if (sInstance == null) {
+ sInstance = new CameraManager();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Sets the surface to use to display the preview This must only be called AFTER the CameraPreview
+ * has a texture ready
+ *
+ * @param preview The preview surface view
+ */
+ void setSurface(final CameraPreview preview) {
+ if (preview == mCameraPreview) {
+ return;
+ }
+
+ if (preview != null) {
+ Assert.checkArgument(preview.isValid());
+ preview.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(final View view, final MotionEvent motionEvent) {
+ if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP)
+ == MotionEvent.ACTION_UP) {
+ mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight());
+ mFocusOverlayManager.onSingleTapUp(
+ (int) motionEvent.getX() + view.getLeft(),
+ (int) motionEvent.getY() + view.getTop());
+ }
+ view.performClick();
+ return true;
+ }
+ });
+ }
+ mCameraPreview = preview;
+ tryShowPreview();
+ }
+
+ public void setRenderOverlay(final RenderOverlay renderOverlay) {
+ mFocusOverlayManager.setFocusRenderer(
+ renderOverlay != null ? renderOverlay.getPieRenderer() : null);
+ }
+
+ /** Convenience function to swap between front and back facing cameras */
+ public void swapCamera() {
+ Assert.checkState(mCameraIndex >= 0);
+ selectCamera(
+ mCameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT
+ ? CameraInfo.CAMERA_FACING_BACK
+ : CameraInfo.CAMERA_FACING_FRONT);
+ }
+
+ /**
+ * Selects the first camera facing the desired direction, or the first camera if there is no
+ * camera in the desired direction
+ *
+ * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants
+ * @return True if a camera was selected, or false if selecting a camera failed
+ */
+ public boolean selectCamera(final int desiredFacing) {
+ try {
+ // We already selected a camera facing that direction
+ if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) {
+ return true;
+ }
+
+ final int cameraCount = Camera.getNumberOfCameras();
+ Assert.checkState(cameraCount > 0);
+
+ mCameraIndex = NO_CAMERA_SELECTED;
+ setCamera(null);
+ final CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == desiredFacing) {
+ mCameraIndex = i;
+ Camera.getCameraInfo(i, mCameraInfo);
+ break;
+ }
+ }
+
+ // There's no camera in the desired facing direction, just select the first camera
+ // regardless of direction
+ if (mCameraIndex < 0) {
+ mCameraIndex = 0;
+ Camera.getCameraInfo(0, mCameraInfo);
+ }
+
+ if (mOpenRequested) {
+ // The camera is open, so reopen with the newly selected camera
+ openCamera();
+ }
+ return true;
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.selectCamera", "RuntimeException in CameraManager.selectCamera", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ return false;
+ }
+ }
+
+ public int getCameraIndex() {
+ return mCameraIndex;
+ }
+
+ public void selectCameraByIndex(final int cameraIndex) {
+ if (mCameraIndex == cameraIndex) {
+ return;
+ }
+
+ try {
+ mCameraIndex = cameraIndex;
+ Camera.getCameraInfo(mCameraIndex, mCameraInfo);
+ if (mOpenRequested) {
+ openCamera();
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.selectCameraByIndex",
+ "RuntimeException in CameraManager.selectCameraByIndex",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public CameraInfo getCameraInfo() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ return null;
+ }
+ return mCameraInfo;
+ }
+
+ /** @return True if the device has both a front and back camera */
+ public boolean hasFrontAndBackCamera() {
+ return mHasFrontAndBackCamera;
+ }
+
+ /** Opens the camera on a separate thread and initiates the preview if one is available */
+ void openCamera() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ // Ensure a selected camera if none is currently selected. This may happen if the
+ // camera chooser is not the default media chooser.
+ selectCamera(CameraInfo.CAMERA_FACING_BACK);
+ }
+ mOpenRequested = true;
+ // We're already opening the camera or already have the camera handle, nothing more to do
+ if (mPendingOpenCameraIndex == mCameraIndex || mCamera != null) {
+ return;
+ }
+
+ // True if the task to open the camera has to be delayed until the current one completes
+ boolean delayTask = false;
+
+ // Cancel any previous open camera tasks
+ if (mOpenCameraTask != null) {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ delayTask = true;
+ }
+
+ mPendingOpenCameraIndex = mCameraIndex;
+ mOpenCameraTask =
+ new AsyncTask<Integer, Void, Camera>() {
+ private Exception mException;
+
+ @Override
+ protected Camera doInBackground(final Integer... params) {
+ try {
+ final int cameraIndex = params[0];
+ LogUtil.v("CameraManager.doInBackground", "Opening camera " + mCameraIndex);
+ return Camera.open(cameraIndex);
+ } catch (final Exception e) {
+ LogUtil.e("CameraManager.doInBackground", "Exception while opening camera", e);
+ mException = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final Camera camera) {
+ // If we completed, but no longer want this camera, then release the camera
+ if (mOpenCameraTask != this || !mOpenRequested) {
+ releaseCamera(camera);
+ cleanup();
+ return;
+ }
+
+ cleanup();
+
+ LogUtil.v(
+ "CameraManager.onPostExecute",
+ "Opened camera " + mCameraIndex + " " + (camera != null));
+ setCamera(camera);
+ if (camera == null) {
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, mException);
+ }
+ LogUtil.e("CameraManager.onPostExecute", "Error opening camera");
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ cleanup();
+ }
+
+ private void cleanup() {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) {
+ // If there's another task waiting on this one to complete, start it now
+ mOpenCameraTask.execute(mCameraIndex);
+ } else {
+ mOpenCameraTask = null;
+ }
+ }
+ };
+ LogUtil.v("CameraManager.openCamera", "Start opening camera " + mCameraIndex);
+ if (!delayTask) {
+ mOpenCameraTask.execute(mCameraIndex);
+ }
+ }
+
+ /** Closes the camera releasing the resources it uses */
+ void closeCamera() {
+ mOpenRequested = false;
+ setCamera(null);
+ }
+
+ /**
+ * Sets the listener which will be notified of errors or other events in the camera
+ *
+ * @param listener The listener to notify
+ */
+ public void setListener(final CameraManagerListener listener) {
+ Assert.isMainThread();
+ mListener = listener;
+ if (!mIsHardwareAccelerationSupported && mListener != null) {
+ mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null);
+ }
+ }
+
+ public void takePicture(final float heightPercent, @NonNull final MediaCallback callback) {
+ Assert.checkState(!mTakingPicture);
+ Assert.isNotNull(callback);
+ mCameraPreview.setFocusable(false);
+ mFocusOverlayManager.cancelAutoFocus();
+ if (mCamera == null) {
+ // The caller should have checked isCameraAvailable first, but just in case, protect
+ // against a null camera by notifying the callback that taking the picture didn't work
+ callback.onMediaFailed(null);
+ return;
+ }
+ final Camera.PictureCallback jpegCallback =
+ new Camera.PictureCallback() {
+ @Override
+ public void onPictureTaken(final byte[] bytes, final Camera camera) {
+ mTakingPicture = false;
+ if (mCamera != camera) {
+ // This may happen if the camera was changed between front/back while the
+ // picture is being taken.
+ callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED);
+ return;
+ }
+
+ if (bytes == null) {
+ callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA);
+ return;
+ }
+
+ final Camera.Size size = camera.getParameters().getPictureSize();
+ int width;
+ int height;
+ if (mRotation == 90 || mRotation == 270) {
+ // Is rotated, so swapping dimensions is desired
+ //noinspection SuspiciousNameCombination
+ width = size.height;
+ //noinspection SuspiciousNameCombination
+ height = size.width;
+ } else {
+ width = size.width;
+ height = size.height;
+ }
+ LogUtil.i(
+ "CameraManager.onPictureTaken", "taken picture size: " + bytes.length + " bytes");
+ new ImagePersistTask(
+ width, height, heightPercent, bytes, mCameraPreview.getContext(), callback)
+ .execute();
+ }
+ };
+
+ mTakingPicture = true;
+ try {
+ mCamera.takePicture(
+ // A shutter callback is required to enable shutter sound
+ DUMMY_SHUTTER_CALLBACK, null /* raw */, null /* postView */, jpegCallback);
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.takePicture", "RuntimeException in CameraManager.takePicture", e);
+ mTakingPicture = false;
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_TAKING_PICTURE, e);
+ }
+ }
+ }
+
+ /**
+ * Asynchronously releases a camera
+ *
+ * @param camera The camera to release
+ */
+ private void releaseCamera(final Camera camera) {
+ if (camera == null) {
+ return;
+ }
+
+ mFocusOverlayManager.onCameraReleased();
+
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... params) {
+ LogUtil.v("CameraManager.doInBackground", "Releasing camera " + mCameraIndex);
+ camera.release();
+ return null;
+ }
+ }.execute();
+ }
+
+ /** Updates the orientation of the camera to match the orientation of the device */
+ private void updateCameraOrientation() {
+ if (mCamera == null || mCameraPreview == null || mTakingPicture) {
+ return;
+ }
+
+ final WindowManager windowManager =
+ (WindowManager) mCameraPreview.getContext().getSystemService(Context.WINDOW_SERVICE);
+
+ int degrees = 0;
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+
+ // The display orientation of the camera (this controls the preview image).
+ int orientation;
+
+ // The clockwise rotation angle relative to the orientation of the camera. This affects
+ // pictures returned by the camera in Camera.PictureCallback.
+ int rotation;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ orientation = (mCameraInfo.orientation + degrees) % 360;
+ rotation = orientation;
+ // compensate the mirror but only for orientation
+ orientation = (360 - orientation) % 360;
+ } else { // back-facing
+ orientation = (mCameraInfo.orientation - degrees + 360) % 360;
+ rotation = orientation;
+ }
+ mRotation = rotation;
+ try {
+ mCamera.setDisplayOrientation(orientation);
+ final Camera.Parameters params = mCamera.getParameters();
+ params.setRotation(rotation);
+ mCamera.setParameters(params);
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.updateCameraOrientation",
+ "RuntimeException in CameraManager.updateCameraOrientation",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ /** Sets the current camera, releasing any previously opened camera */
+ private void setCamera(final Camera camera) {
+ if (mCamera == camera) {
+ return;
+ }
+
+ releaseCamera(mCamera);
+ mCamera = camera;
+ tryShowPreview();
+ if (mListener != null) {
+ mListener.onCameraChanged();
+ }
+ }
+
+ /** Shows the preview if the camera is open and the preview is loaded */
+ private void tryShowPreview() {
+ if (mCameraPreview == null || mCamera == null) {
+ if (mOrientationHandler != null) {
+ mOrientationHandler.disable();
+ mOrientationHandler = null;
+ }
+ // releaseMediaRecorder(true /* cleanupFile */);
+ mFocusOverlayManager.onPreviewStopped();
+ return;
+ }
+ try {
+ mCamera.stopPreview();
+ updateCameraOrientation();
+
+ final Camera.Parameters params = mCamera.getParameters();
+ final Camera.Size pictureSize = chooseBestPictureSize();
+ final Camera.Size previewSize = chooseBestPreviewSize(pictureSize);
+ params.setPreviewSize(previewSize.width, previewSize.height);
+ params.setPictureSize(pictureSize.width, pictureSize.height);
+ logCameraSize("Setting preview size: ", previewSize);
+ logCameraSize("Setting picture size: ", pictureSize);
+ mCameraPreview.setSize(previewSize, mCameraInfo.orientation);
+ for (final String focusMode : params.getSupportedFocusModes()) {
+ if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ // Use continuous focus if available
+ params.setFocusMode(focusMode);
+ break;
+ }
+ }
+
+ mCamera.setParameters(params);
+ mCameraPreview.startPreview(mCamera);
+ mCamera.startPreview();
+ mCamera.setAutoFocusMoveCallback(
+ new Camera.AutoFocusMoveCallback() {
+ @Override
+ public void onAutoFocusMoving(final boolean start, final Camera camera) {
+ mFocusOverlayManager.onAutoFocusMoving(start);
+ }
+ });
+ mFocusOverlayManager.setParameters(mCamera.getParameters());
+ mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK);
+ mFocusOverlayManager.onPreviewStarted();
+ if (mOrientationHandler == null) {
+ mOrientationHandler = new OrientationHandler(mCameraPreview.getContext());
+ mOrientationHandler.enable();
+ }
+ } catch (final IOException e) {
+ LogUtil.e("CameraManager.tryShowPreview", "IOException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.tryShowPreview", "RuntimeException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ }
+ }
+
+ public boolean isCameraAvailable() {
+ return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported;
+ }
+
+ /**
+ * Choose the best picture size by trying to find a size close to the MmsConfig's max size, which
+ * is closest to the screen aspect ratio. In case of RCS conversation returns default size.
+ */
+ private Camera.Size chooseBestPictureSize() {
+ return mCamera.getParameters().getPictureSize();
+ }
+
+ /**
+ * Chose the best preview size based on the picture size. Try to find a size with the same aspect
+ * ratio and size as the picture if possible
+ */
+ private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) {
+ final List<Camera.Size> sizes =
+ new ArrayList<Camera.Size>(mCamera.getParameters().getSupportedPreviewSizes());
+ final float aspectRatio = pictureSize.width / (float) pictureSize.height;
+ final int capturePixels = pictureSize.width * pictureSize.height;
+
+ // Sort the sizes so the best size is first
+ Collections.sort(
+ sizes,
+ new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, aspectRatio, capturePixels));
+
+ return sizes.get(0);
+ }
+
+ private class OrientationHandler extends OrientationEventListener {
+ OrientationHandler(final Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(final int orientation) {
+ updateCameraOrientation();
+ }
+ }
+
+ private static class SizeComparator implements Comparator<Camera.Size> {
+ private static final int PREFER_LEFT = -1;
+ private static final int PREFER_RIGHT = 1;
+
+ // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ // The desired aspect ratio
+ private final float mTargetAspectRatio;
+
+ // The desired size (width x height) to try to match
+ private final int mTargetPixels;
+
+ public SizeComparator(
+ final int maxWidth,
+ final int maxHeight,
+ final float targetAspectRatio,
+ final int targetPixels) {
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ mTargetAspectRatio = targetAspectRatio;
+ mTargetPixels = targetPixels;
+ }
+
+ /**
+ * Returns a negative value if left is a better choice than right, or a positive value if right
+ * is a better choice is better than left. 0 if they are equal
+ */
+ @Override
+ public int compare(final Camera.Size left, final Camera.Size right) {
+ // If one size is less than the max size prefer it over the other
+ if ((left.width <= mMaxWidth && left.height <= mMaxHeight)
+ != (right.width <= mMaxWidth && right.height <= mMaxHeight)) {
+ return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // If one is closer to the target aspect ratio, prefer it.
+ final float leftAspectRatio = left.width / (float) left.height;
+ final float rightAspectRatio = right.width / (float) right.height;
+ final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio);
+ final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio);
+ if (leftAspectRatioDiff != rightAspectRatioDiff) {
+ return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // At this point they have the same aspect ratio diff and are either both bigger
+ // than the max size or both smaller than the max size, so prefer the one closest
+ // to target size
+ final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels);
+ final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels);
+ return leftDiff - rightDiff;
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void autoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+
+ try {
+ mCamera.autoFocus(
+ new Camera.AutoFocusCallback() {
+ @Override
+ public void onAutoFocus(final boolean success, final Camera camera) {
+ mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */);
+ }
+ });
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.autoFocus", "RuntimeException in CameraManager.autoFocus", e);
+ // If autofocus fails, the camera should have called the callback with success=false,
+ // but some throw an exception here
+ mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void cancelAutoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ mCamera.cancelAutoFocus();
+ } catch (final RuntimeException e) {
+ // Ignore
+ LogUtil.e(
+ "CameraManager.cancelAutoFocus", "RuntimeException in CameraManager.cancelAutoFocus", e);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public boolean capture() {
+ return false;
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void setFocusParameters() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ final Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setFocusMode(mFocusOverlayManager.getFocusMode());
+ if (parameters.getMaxNumFocusAreas() > 0) {
+ // Don't set focus areas (even to null) if focus areas aren't supported, camera may
+ // crash
+ parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas());
+ }
+ parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas());
+ mCamera.setParameters(parameters);
+ } catch (final RuntimeException e) {
+ // This occurs when the device is out of space or when the camera is locked
+ LogUtil.e(
+ "CameraManager.setFocusParameters",
+ "RuntimeException in CameraManager setFocusParameters");
+ }
+ }
+
+ public void resetPreview() {
+ mCamera.startPreview();
+ if (mCameraPreview != null) {
+ mCameraPreview.setFocusable(true);
+ }
+ }
+
+ private void logCameraSize(final String prefix, final Camera.Size size) {
+ // Log the camera size and aspect ratio for help when examining bug reports for camera
+ // failures
+ LogUtil.i(
+ "CameraManager.logCameraSize",
+ prefix + size.width + "x" + size.height + " (" + (size.width / (float) size.height) + ")");
+ }
+
+ @VisibleForTesting
+ public void resetCameraManager() {
+ sInstance = null;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/CameraPreview.java b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
new file mode 100644
index 000000000..6581ad67b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.hardware.Camera;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.IOException;
+
+/**
+ * Contains shared code for SoftwareCameraPreview and HardwareCameraPreview, cannot use inheritance
+ * because those classes must inherit from separate Views, so those classes delegate calls to this
+ * helper class. Specifics for each implementation are in CameraPreviewHost
+ */
+public class CameraPreview {
+ /** Implemented by the camera for rendering. */
+ public interface CameraPreviewHost {
+ View getView();
+
+ boolean isValid();
+
+ void startPreview(final Camera camera) throws IOException;
+
+ void onCameraPermissionGranted();
+
+ void setShown();
+ }
+
+ private int mCameraWidth = -1;
+ private int mCameraHeight = -1;
+ private boolean mTabHasBeenShown = false;
+ private OnTouchListener mListener;
+
+ private final CameraPreviewHost mHost;
+
+ public CameraPreview(final CameraPreviewHost host) {
+ Assert.isNotNull(host);
+ Assert.isNotNull(host.getView());
+ mHost = host;
+ }
+
+ // This is set when the tab is actually selected.
+ public void setShown() {
+ mTabHasBeenShown = true;
+ maybeOpenCamera();
+ }
+
+ // Opening camera is very expensive. Most of the ANR reports seem to be related to the camera.
+ // So we delay until the camera is actually needed. See b/23287938
+ private void maybeOpenCamera() {
+ boolean visible = mHost.getView().getVisibility() == View.VISIBLE;
+ if (mTabHasBeenShown && visible && PermissionsUtil.hasCameraPermissions(getContext())) {
+ CameraManager.get().openCamera();
+ }
+ }
+
+ public void setSize(final Camera.Size size, final int orientation) {
+ switch (orientation) {
+ case 0:
+ case 180:
+ mCameraWidth = size.width;
+ mCameraHeight = size.height;
+ break;
+ case 90:
+ case 270:
+ default:
+ mCameraWidth = size.height;
+ mCameraHeight = size.width;
+ }
+ mHost.getView().requestLayout();
+ }
+
+ public int getWidthMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ return MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ } else {
+ return widthMeasureSpec;
+ }
+ }
+
+ public int getHeightMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int orientation = getContext().getResources().getConfiguration().orientation;
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ final float aspectRatio = (float) mCameraWidth / (float) mCameraHeight;
+ int height;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ height = (int) (width * aspectRatio);
+ } else {
+ height = (int) (width / aspectRatio);
+ }
+ return View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ } else {
+ return heightMeasureSpec;
+ }
+ }
+
+ // onVisibilityChanged is set to Visible when the tab is _created_,
+ // which may be when the user is viewing a different tab.
+ public void onVisibilityChanged(final int visibility) {
+ if (PermissionsUtil.hasCameraPermissions(getContext())) {
+ if (visibility == View.VISIBLE) {
+ maybeOpenCamera();
+ } else {
+ CameraManager.get().closeCamera();
+ }
+ }
+ }
+
+ public Context getContext() {
+ return mHost.getView().getContext();
+ }
+
+ public void setOnTouchListener(final View.OnTouchListener listener) {
+ mListener = listener;
+ mHost.getView().setOnTouchListener(listener);
+ }
+
+ public void setFocusable(boolean focusable) {
+ mHost.getView().setOnTouchListener(focusable ? mListener : null);
+ }
+
+ public int getHeight() {
+ return mHost.getView().getHeight();
+ }
+
+ public void onAttachedToWindow() {
+ maybeOpenCamera();
+ }
+
+ public void onDetachedFromWindow() {
+ CameraManager.get().closeCamera();
+ }
+
+ public void onRestoreInstanceState() {
+ maybeOpenCamera();
+ }
+
+ public void onCameraPermissionGranted() {
+ maybeOpenCamera();
+ }
+
+ /** @return True if the view is valid and prepared for the camera to start showing the preview */
+ public boolean isValid() {
+ return mHost.isValid();
+ }
+
+ /**
+ * Starts the camera preview on the current surface. Abstracts out the differences in API from the
+ * CameraManager
+ *
+ * @throws IOException Which is caught by the CameraManager to display an error
+ */
+ public void startPreview(final Camera camera) throws IOException {
+ mHost.startPreview(camera);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
new file mode 100644
index 000000000..c0d61f3f8
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.TextureView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A hardware accelerated preview texture for the camera. This is the preferred CameraPreview
+ * because it animates smoother. When hardware acceleration isn't available, SoftwareCameraPreview
+ * is used.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class HardwareCameraPreview extends TextureView implements CameraPreview.CameraPreviewHost {
+ private CameraPreview mPreview;
+
+ public HardwareCameraPreview(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mPreview = new CameraPreview(this);
+ setSurfaceTextureListener(
+ new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(null);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(mPreview);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getSurfaceTexture() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewTexture(getSurfaceTexture());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
new file mode 100644
index 000000000..150009495
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.content.FileProvider;
+import com.android.dialer.callcomposer.camera.exif.ExifInterface;
+import com.android.dialer.callcomposer.camera.exif.ExifTag;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.util.DialerUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Persisting image routine. */
+@TargetApi(VERSION_CODES.M)
+public class ImagePersistTask extends FallibleAsyncTask<Void, Void, Uri> {
+ private int mWidth;
+ private int mHeight;
+ private final float mHeightPercent;
+ private final byte[] mBytes;
+ private final Context mContext;
+ private final CameraManager.MediaCallback mCallback;
+
+ ImagePersistTask(
+ final int width,
+ final int height,
+ final float heightPercent,
+ final byte[] bytes,
+ final Context context,
+ final CameraManager.MediaCallback callback) {
+ Assert.checkArgument(heightPercent >= 0 && heightPercent <= 1);
+ Assert.isNotNull(bytes);
+ Assert.isNotNull(context);
+ Assert.isNotNull(callback);
+ mWidth = width;
+ mHeight = height;
+ mHeightPercent = heightPercent;
+ mBytes = bytes;
+ mContext = context;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Uri doInBackgroundFallible(final Void... params) throws Exception {
+ File outputFile = DialerUtils.createShareableFile(mContext);
+
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
+ if (mHeightPercent != 1.0f) {
+ writeClippedBitmap(outputStream);
+ } else {
+ outputStream.write(mBytes, 0, mBytes.length);
+ }
+ }
+
+ return FileProvider.getUriForFile(
+ mContext, Constants.get().getFileProviderAuthority(), outputFile);
+ }
+
+ @Override
+ protected void onPostExecute(FallibleTaskResult<Uri> result) {
+ if (result.isFailure()) {
+ mCallback.onMediaFailed(new Exception("Persisting image failed", result.getThrowable()));
+ } else {
+ mCallback.onMediaReady(result.getResult(), "image/jpeg", mWidth, mHeight);
+ }
+ }
+
+ private void writeClippedBitmap(OutputStream outputStream) throws IOException {
+ int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
+ final ExifInterface exifInterface = new ExifInterface();
+ try {
+ exifInterface.readExif(mBytes);
+ final Integer orientationValue = exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (orientationValue != null) {
+ orientation = orientationValue.intValue();
+ }
+ } catch (final IOException e) {
+ // Couldn't get exif tags, not the end of the world
+ }
+ Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
+ final int clippedWidth;
+ final int clippedHeight;
+ if (ExifInterface.getOrientationParams(orientation).invertDimensions) {
+ Assert.checkState(mWidth == bitmap.getHeight());
+ Assert.checkState(mHeight == bitmap.getWidth());
+ clippedWidth = (int) (mHeight * mHeightPercent);
+ clippedHeight = mWidth;
+ } else {
+ Assert.checkState(mWidth == bitmap.getWidth());
+ Assert.checkState(mHeight == bitmap.getHeight());
+ clippedWidth = mWidth;
+ clippedHeight = (int) (mHeight * mHeightPercent);
+ }
+ final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2;
+ final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2;
+ mWidth = clippedWidth;
+ mHeight = clippedHeight;
+ Bitmap clippedBitmap =
+ Bitmap.createBitmap(clippedWidth, clippedHeight, Bitmap.Config.ARGB_8888);
+ clippedBitmap.setDensity(bitmap.getDensity());
+ final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap);
+ final Matrix matrix = new Matrix();
+ matrix.postTranslate(-offsetLeft, -offsetTop);
+ clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */);
+ clippedBitmapCanvas.save();
+ clippedBitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(clippedBitmap);
+ // EXIF data can take a big chunk of the file size and is often cleared by the
+ // carrier, only store orientation since that's critical
+ final ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION);
+ exifInterface.clearExif();
+ exifInterface.setTag(orientationTag);
+ exifInterface.writeExif(clippedBitmap, outputStream);
+
+ clippedBitmap.recycle();
+ bitmap.recycle();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
new file mode 100644
index 000000000..fe2c600df
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A software rendered preview surface for the camera. This renders slower and causes more jank, so
+ * HardwareCameraPreview is preferred if possible.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class SoftwareCameraPreview extends SurfaceView implements CameraPreview.CameraPreviewHost {
+ private final CameraPreview mPreview;
+
+ public SoftwareCameraPreview(final Context context) {
+ super(context);
+ mPreview = new CameraPreview(this);
+ getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceChanged(
+ final SurfaceHolder surfaceHolder,
+ final int format,
+ final int width,
+ final int height) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceDestroyed(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(null);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getHolder() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewDisplay(getHolder());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
new file mode 100644
index 000000000..77ef22295
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.callcomposer.camera.camerafocus"/> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java
new file mode 100644
index 000000000..234cf5388
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+/** Methods needed by the CameraPreview in order communicate camera events. */
+public interface FocusIndicator {
+ void showStart();
+
+ void showSuccess(boolean timeout);
+
+ void showFail(boolean timeout);
+
+ void clear();
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
new file mode 100644
index 000000000..1c5ac380c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera.Area;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that handles everything about focus in still picture mode. This also handles the metering
+ * area because it is the same as focus area.
+ *
+ * <p>The test cases: (1) The camera has continuous autofocus. Move the camera. Take a picture when
+ * CAF is not in progress. (2) The camera has continuous autofocus. Move the camera. Take a picture
+ * when CAF is in progress. (3) The camera has face detection. Point the camera at some faces. Hold
+ * the shutter. Release to take a picture. (4) The camera has face detection. Point the camera at
+ * some faces. Single tap the shutter to take a picture. (5) The camera has autofocus. Single tap
+ * the shutter to take a picture. (6) The camera has autofocus. Hold the shutter. Release to take a
+ * picture. (7) The camera has no autofocus. Single tap the shutter and take a picture. (8) The
+ * camera has autofocus and supports focus area. Touch the screen to trigger autofocus. Take a
+ * picture. (9) The camera has autofocus and supports focus area. Touch the screen to trigger
+ * autofocus. Wait until it times out. (10) The camera has no autofocus and supports metering area.
+ * Touch the screen to change metering area.
+ */
+public class FocusOverlayManager {
+ private static final String TRUE = "true";
+ private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+ private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED =
+ "auto-whitebalance-lock-supported";
+
+ private static final int RESET_TOUCH_FOCUS = 0;
+ private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
+
+ private int mState = STATE_IDLE;
+ private static final int STATE_IDLE = 0; // Focus is not active.
+ private static final int STATE_FOCUSING = 1; // Focus is in progress.
+ // Focus is in progress and the camera should take a picture after focus finishes.
+ private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
+ private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
+ private static final int STATE_FAIL = 4; // Focus finishes and fails.
+
+ private boolean mInitialized;
+ private boolean mFocusAreaSupported;
+ private boolean mMeteringAreaSupported;
+ private boolean mLockAeAwbNeeded;
+ private boolean mAeAwbLock;
+ private Matrix mMatrix;
+
+ private PieRenderer mPieRenderer;
+
+ private int mPreviewWidth; // The width of the preview frame layout.
+ private int mPreviewHeight; // The height of the preview frame layout.
+ private boolean mMirror; // true if the camera is front-facing.
+ private List<Area> mFocusArea; // focus area in driver format
+ private List<Area> mMeteringArea; // metering area in driver format
+ private String mFocusMode;
+ private Parameters mParameters;
+ private Handler mHandler;
+ private Listener mListener;
+
+ /** Listener used for the focus indicator to communicate back to the camera. */
+ public interface Listener {
+ void autoFocus();
+
+ void cancelAutoFocus();
+
+ boolean capture();
+
+ void setFocusParameters();
+ }
+
+ private class MainHandler extends Handler {
+ public MainHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RESET_TOUCH_FOCUS:
+ {
+ cancelAutoFocus();
+ break;
+ }
+ }
+ }
+ }
+
+ public FocusOverlayManager(Listener listener, Looper looper) {
+ mHandler = new MainHandler(looper);
+ mMatrix = new Matrix();
+ mListener = listener;
+ }
+
+ public void setFocusRenderer(PieRenderer renderer) {
+ mPieRenderer = renderer;
+ mInitialized = (mMatrix != null);
+ }
+
+ public void setParameters(Parameters parameters) {
+ // parameters can only be null when onConfigurationChanged is called
+ // before camera is open. We will just return in this case, because
+ // parameters will be set again later with the right parameters after
+ // camera is open.
+ if (parameters == null) {
+ return;
+ }
+ mParameters = parameters;
+ mFocusAreaSupported = isFocusAreaSupported(parameters);
+ mMeteringAreaSupported = isMeteringAreaSupported(parameters);
+ mLockAeAwbNeeded =
+ (isAutoExposureLockSupported(mParameters) || isAutoWhiteBalanceLockSupported(mParameters));
+ }
+
+ public void setPreviewSize(int previewWidth, int previewHeight) {
+ if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ setMatrix();
+ }
+ }
+
+ public void setMirror(boolean mirror) {
+ mMirror = mirror;
+ setMatrix();
+ }
+
+ private void setMatrix() {
+ if (mPreviewWidth != 0 && mPreviewHeight != 0) {
+ Matrix matrix = new Matrix();
+ prepareMatrix(matrix, mMirror, mPreviewWidth, mPreviewHeight);
+ // In face detection, the matrix converts the driver coordinates to UI
+ // coordinates. In tap focus, the inverted matrix converts the UI
+ // coordinates to driver coordinates.
+ matrix.invert(mMatrix);
+ mInitialized = (mPieRenderer != null);
+ }
+ }
+
+ private void lockAeAwbIfNeeded() {
+ if (mLockAeAwbNeeded && !mAeAwbLock) {
+ mAeAwbLock = true;
+ mListener.setFocusParameters();
+ }
+ }
+
+ public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
+ if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ // Take the picture no matter focus succeeds or fails. No need
+ // to play the AF sound if we're about to play the shutter
+ // sound.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ capture();
+ } else if (mState == STATE_FOCUSING) {
+ // This happens when (1) user is half-pressing the focus key or
+ // (2) touch focus is triggered. Play the focus tone. Do not
+ // take the picture now.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ // If this is triggered by touch focus, cancel focus after a
+ // while.
+ if (mFocusArea != null) {
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ if (shutterButtonPressed) {
+ // Lock AE & AWB so users can half-press shutter and recompose.
+ lockAeAwbIfNeeded();
+ }
+ } else if (mState == STATE_IDLE) {
+ // User has released the focus key before focus completes.
+ // Do nothing.
+ }
+ }
+
+ public void onAutoFocusMoving(boolean moving) {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Ignore if we have requested autofocus. This method only handles
+ // continuous autofocus.
+ if (mState != STATE_IDLE) {
+ return;
+ }
+
+ if (moving) {
+ mPieRenderer.showStart();
+ } else {
+ mPieRenderer.showSuccess(true);
+ }
+ }
+
+ private void initializeFocusAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mFocusArea == null) {
+ mFocusArea = new ArrayList<>();
+ mFocusArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ calculateTapArea(
+ focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, mFocusArea.get(0).rect);
+ }
+
+ private void initializeMeteringAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mMeteringArea == null) {
+ mMeteringArea = new ArrayList<>();
+ mMeteringArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ // AE area is bigger because exposure is sensitive and
+ // easy to over- or underexposure if area is too small.
+ calculateTapArea(
+ focusWidth,
+ focusHeight,
+ 1.5f,
+ x,
+ y,
+ previewWidth,
+ previewHeight,
+ mMeteringArea.get(0).rect);
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ return;
+ }
+
+ // Let users be able to cancel previous touch focus.
+ if ((mFocusArea != null)
+ && (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+ cancelAutoFocus();
+ }
+ // Initialize variables.
+ int focusWidth = mPieRenderer.getSize();
+ int focusHeight = mPieRenderer.getSize();
+ if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) {
+ return;
+ }
+ int previewWidth = mPreviewWidth;
+ int previewHeight = mPreviewHeight;
+ // Initialize mFocusArea.
+ if (mFocusAreaSupported) {
+ initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+ // Initialize mMeteringArea.
+ if (mMeteringAreaSupported) {
+ initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+
+ // Use margin to set the focus indicator to the touched area.
+ mPieRenderer.setFocus(x, y);
+
+ // Set the focus area and metering area.
+ mListener.setFocusParameters();
+ if (mFocusAreaSupported) {
+ autoFocus();
+ } else { // Just show the indicator in all other cases.
+ updateFocusUI();
+ // Reset the metering area in 3 seconds.
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ }
+
+ public void onPreviewStarted() {
+ mState = STATE_IDLE;
+ }
+
+ public void onPreviewStopped() {
+ // If auto focus was in progress, it would have been stopped.
+ mState = STATE_IDLE;
+ resetTouchFocus();
+ updateFocusUI();
+ }
+
+ public void onCameraReleased() {
+ onPreviewStopped();
+ }
+
+ private void autoFocus() {
+ LogUtil.v("FocusOverlayManager.autoFocus", "Start autofocus.");
+ mListener.autoFocus();
+ mState = STATE_FOCUSING;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ public void cancelAutoFocus() {
+ LogUtil.v("FocusOverlayManager.cancelAutoFocus", "Cancel autofocus.");
+
+ // Reset the tap area before calling mListener.cancelAutofocus.
+ // Otherwise, focus mode stays at auto and the tap area passed to the
+ // driver is not reset.
+ resetTouchFocus();
+ mListener.cancelAutoFocus();
+ mState = STATE_IDLE;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ private void capture() {
+ if (mListener.capture()) {
+ mState = STATE_IDLE;
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+ }
+
+ public String getFocusMode() {
+ List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+ if (mFocusAreaSupported && mFocusArea != null) {
+ // Always use autofocus in tap-to-focus.
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
+ }
+
+ if (!isSupported(mFocusMode, supportedFocusModes)) {
+ // For some reasons, the driver does not support the current
+ // focus mode. Fall back to auto.
+ if (isSupported(Parameters.FOCUS_MODE_AUTO, mParameters.getSupportedFocusModes())) {
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = mParameters.getFocusMode();
+ }
+ }
+ return mFocusMode;
+ }
+
+ public List<Area> getFocusAreas() {
+ return mFocusArea;
+ }
+
+ public List<Area> getMeteringAreas() {
+ return mMeteringArea;
+ }
+
+ private void updateFocusUI() {
+ if (!mInitialized) {
+ return;
+ }
+ FocusIndicator focusIndicator = mPieRenderer;
+
+ if (mState == STATE_IDLE) {
+ if (mFocusArea == null) {
+ focusIndicator.clear();
+ } else {
+ // Users touch on the preview and the indicator represents the
+ // metering area. Either focus area is not supported or
+ // autoFocus call is not required.
+ focusIndicator.showStart();
+ }
+ } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ focusIndicator.showStart();
+ } else {
+ if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+ // TODO: check HAL behavior and decide if this can be removed.
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_SUCCESS) {
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_FAIL) {
+ focusIndicator.showFail(false);
+ }
+ }
+ }
+
+ private void resetTouchFocus() {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Put focus indicator to the center. clear reset position
+ mPieRenderer.clear();
+
+ mFocusArea = null;
+ mMeteringArea = null;
+ }
+
+ private void calculateTapArea(
+ int focusWidth,
+ int focusHeight,
+ float areaMultiple,
+ int x,
+ int y,
+ int previewWidth,
+ int previewHeight,
+ Rect rect) {
+ int areaWidth = (int) (focusWidth * areaMultiple);
+ int areaHeight = (int) (focusHeight * areaMultiple);
+ final int maxW = previewWidth - areaWidth;
+ int left = maxW > 0 ? clamp(x - areaWidth / 2, 0, maxW) : 0;
+ final int maxH = previewHeight - areaHeight;
+ int top = maxH > 0 ? clamp(y - areaHeight / 2, 0, maxH) : 0;
+
+ RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
+ mMatrix.mapRect(rectF);
+ rectFToRect(rectF, rect);
+ }
+
+ private int clamp(int x, int min, int max) {
+ Assert.checkArgument(max >= min);
+ if (x > max) {
+ return max;
+ }
+ if (x < min) {
+ return min;
+ }
+ return x;
+ }
+
+ private boolean isAutoExposureLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
+ }
+
+ private boolean isAutoWhiteBalanceLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
+ }
+
+ private boolean isSupported(String value, List<String> supported) {
+ return supported != null && supported.indexOf(value) >= 0;
+ }
+
+ private boolean isMeteringAreaSupported(Parameters params) {
+ return params.getMaxNumMeteringAreas() > 0;
+ }
+
+ private boolean isFocusAreaSupported(Parameters params) {
+ return (params.getMaxNumFocusAreas() > 0
+ && isSupported(Parameters.FOCUS_MODE_AUTO, params.getSupportedFocusModes()));
+ }
+
+ private void prepareMatrix(Matrix matrix, boolean mirror, int viewWidth, int viewHeight) {
+ // Need mirror for front camera.
+ matrix.setScale(mirror ? -1 : 1, 1);
+ // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
+ // UI coordinates range from (0, 0) to (width, height).
+ matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
+ matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
+ }
+
+ private void rectFToRect(RectF rectF, Rect rect) {
+ rect.left = Math.round(rectF.left);
+ rect.top = Math.round(rectF.top);
+ rect.right = Math.round(rectF.right);
+ rect.bottom = Math.round(rectF.bottom);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
new file mode 100644
index 000000000..4a3b522bb
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+/** Abstract class that all Camera overlays should implement. */
+public abstract class OverlayRenderer implements RenderOverlay.Renderer {
+
+ protected RenderOverlay mOverlay;
+
+ private int mLeft;
+ private int mTop;
+ private int mRight;
+ private int mBottom;
+ private boolean mVisible;
+
+ public void setVisible(boolean vis) {
+ mVisible = vis;
+ update();
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ // default does not handle touch
+ @Override
+ public boolean handlesTouch() {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ return false;
+ }
+
+ public abstract void onDraw(Canvas canvas);
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mVisible) {
+ onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ @Override
+ public void layout(int left, int top, int right, int bottom) {
+ mLeft = left;
+ mRight = right;
+ mTop = top;
+ mBottom = bottom;
+ }
+
+ protected Context getContext() {
+ if (mOverlay != null) {
+ return mOverlay.getContext();
+ } else {
+ return null;
+ }
+ }
+
+ public int getWidth() {
+ return mRight - mLeft;
+ }
+
+ public int getHeight() {
+ return mBottom - mTop;
+ }
+
+ protected void update() {
+ if (mOverlay != null) {
+ mOverlay.update();
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
new file mode 100644
index 000000000..86f69c0ae
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import java.util.List;
+
+/** Pie menu item. */
+public class PieItem {
+
+ /** Listener to detect pie item clicks. */
+ public interface OnClickListener {
+ void onClick(PieItem item);
+ }
+
+ private Drawable mDrawable;
+ private int level;
+ private float mCenter;
+ private float start;
+ private float sweep;
+ private float animate;
+ private int inner;
+ private int outer;
+ private boolean mSelected;
+ private boolean mEnabled;
+ private List<PieItem> mItems;
+ private Path mPath;
+ private OnClickListener mOnClickListener;
+ private float mAlpha;
+
+ // Gray out the view when disabled
+ private static final float ENABLED_ALPHA = 1;
+ private static final float DISABLED_ALPHA = (float) 0.3;
+
+ public PieItem(Drawable drawable, int level) {
+ mDrawable = drawable;
+ this.level = level;
+ setAlpha(1f);
+ mEnabled = true;
+ setAnimationAngle(getAnimationAngle());
+ start = -1;
+ mCenter = -1;
+ }
+
+ public boolean hasItems() {
+ return mItems != null;
+ }
+
+ public List<PieItem> getItems() {
+ return mItems;
+ }
+
+ public void setPath(Path p) {
+ mPath = p;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ mDrawable.setAlpha((int) (255 * alpha));
+ }
+
+ public void setAnimationAngle(float a) {
+ animate = a;
+ }
+
+ private float getAnimationAngle() {
+ return animate;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ if (mEnabled) {
+ setAlpha(ENABLED_ALPHA);
+ } else {
+ setAlpha(DISABLED_ALPHA);
+ }
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setSelected(boolean s) {
+ mSelected = s;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public void setGeometry(float st, float sw, int inside, int outside) {
+ start = st;
+ sweep = sw;
+ inner = inside;
+ outer = outside;
+ }
+
+ public float getCenter() {
+ return mCenter;
+ }
+
+ public float getStart() {
+ return start;
+ }
+
+ public float getStartAngle() {
+ return start + animate;
+ }
+
+ public float getSweep() {
+ return sweep;
+ }
+
+ public int getInnerRadius() {
+ return inner;
+ }
+
+ public int getOuterRadius() {
+ return outer;
+ }
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void performClick() {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ }
+ }
+
+ public int getIntrinsicWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mDrawable.setBounds(left, top, right, bottom);
+ }
+
+ public void draw(Canvas canvas) {
+ mDrawable.draw(canvas);
+ }
+
+ public void setImageResource(Context context, int resId) {
+ Drawable d = context.getResources().getDrawable(resId).mutate();
+ d.setBounds(mDrawable.getBounds());
+ mDrawable = d;
+ setAlpha(mAlpha);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
new file mode 100644
index 000000000..59b57b002
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
@@ -0,0 +1,816 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Used to draw and render the pie item focus indicator. */
+public class PieRenderer extends OverlayRenderer implements FocusIndicator {
+ // Sometimes continuous autofocus starts and stops several times quickly.
+ // These states are used to make sure the animation is run for at least some
+ // time.
+ private volatile int mState;
+ private ScaleAnimation mAnimation = new ScaleAnimation();
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_FOCUSING = 1;
+ private static final int STATE_FINISHING = 2;
+ private static final int STATE_PIE = 8;
+
+ private Runnable mDisappear = new Disappear();
+ private Animation.AnimationListener mEndAction = new EndAction();
+ private static final int SCALING_UP_TIME = 600;
+ private static final int SCALING_DOWN_TIME = 100;
+ private static final int DISAPPEAR_TIMEOUT = 200;
+ private static final int DIAL_HORIZONTAL = 157;
+
+ private static final long PIE_FADE_IN_DURATION = 200;
+ private static final long PIE_XFADE_DURATION = 200;
+ private static final long PIE_SELECT_FADE_DURATION = 300;
+
+ private static final int MSG_OPEN = 0;
+ private static final int MSG_CLOSE = 1;
+ private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3);
+ // geometry
+ private Point mCenter;
+ private int mRadius;
+ private int mRadiusInc;
+
+ // the detection if touch is inside a slice is offset
+ // inbounds by this amount to allow the selection to show before the
+ // finger covers it
+ private int mTouchOffset;
+
+ private List<PieItem> mItems;
+
+ private PieItem mOpenItem;
+
+ private Paint mSelectedPaint;
+ private Paint mSubPaint;
+
+ // touch handling
+ private PieItem mCurrentItem;
+
+ private Paint mFocusPaint;
+ private int mSuccessColor;
+ private int mFailColor;
+ private int mCircleSize;
+ private int mFocusX;
+ private int mFocusY;
+ private int mCenterX;
+ private int mCenterY;
+
+ private int mDialAngle;
+ private RectF mCircle;
+ private RectF mDial;
+ private Point mPoint1;
+ private Point mPoint2;
+ private int mStartAnimationAngle;
+ private boolean mFocused;
+ private int mInnerOffset;
+ private int mOuterStroke;
+ private int mInnerStroke;
+ private boolean mTapMode;
+ private boolean mBlockFocus;
+ private int mTouchSlopSquared;
+ private Point mDown;
+ private boolean mOpening;
+ private LinearAnimation mXFade;
+ private LinearAnimation mFadeIn;
+ private volatile boolean mFocusCancelled;
+
+ private Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_OPEN:
+ if (mListener != null) {
+ mListener.onPieOpened(mCenter.x, mCenter.y);
+ }
+ break;
+ case MSG_CLOSE:
+ if (mListener != null) {
+ mListener.onPieClosed();
+ }
+ break;
+ }
+ }
+ };
+
+ private PieListener mListener;
+
+ /** Listener for the pie item to communicate back to the renderer. */
+ public interface PieListener {
+ void onPieOpened(int centerX, int centerY);
+
+ void onPieClosed();
+ }
+
+ public void setPieListener(PieListener pl) {
+ mListener = pl;
+ }
+
+ public PieRenderer(Context context) {
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ setVisible(false);
+ mItems = new ArrayList<PieItem>();
+ Resources res = ctx.getResources();
+ mRadius = res.getDimensionPixelSize(R.dimen.pie_radius_start);
+ mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+ mRadiusInc = res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+ mTouchOffset = res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+ mCenter = new Point(0, 0);
+ mSelectedPaint = new Paint();
+ mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
+ mSelectedPaint.setAntiAlias(true);
+ mSubPaint = new Paint();
+ mSubPaint.setAntiAlias(true);
+ mSubPaint.setColor(Color.argb(200, 250, 230, 128));
+ mFocusPaint = new Paint();
+ mFocusPaint.setAntiAlias(true);
+ mFocusPaint.setColor(Color.WHITE);
+ mFocusPaint.setStyle(Paint.Style.STROKE);
+ mSuccessColor = Color.GREEN;
+ mFailColor = Color.RED;
+ mCircle = new RectF();
+ mDial = new RectF();
+ mPoint1 = new Point();
+ mPoint2 = new Point();
+ mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
+ mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+ mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+ mState = STATE_IDLE;
+ mBlockFocus = false;
+ mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
+ mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
+ mDown = new Point();
+ }
+
+ public boolean showsItems() {
+ return mTapMode;
+ }
+
+ public void addItem(PieItem item) {
+ // add the item to the pie itself
+ mItems.add(item);
+ }
+
+ public void removeItem(PieItem item) {
+ mItems.remove(item);
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ public void showInCenter() {
+ if ((mState == STATE_PIE) && isVisible()) {
+ mTapMode = false;
+ show(false);
+ } else {
+ if (mState != STATE_IDLE) {
+ cancelFocus();
+ }
+ mState = STATE_PIE;
+ setCenter(mCenterX, mCenterY);
+ mTapMode = true;
+ show(true);
+ }
+ }
+
+ public void hide() {
+ show(false);
+ }
+
+ /**
+ * guaranteed has center set
+ *
+ * @param show
+ */
+ private void show(boolean show) {
+ if (show) {
+ mState = STATE_PIE;
+ // ensure clean state
+ mCurrentItem = null;
+ mOpenItem = null;
+ for (PieItem item : mItems) {
+ item.setSelected(false);
+ }
+ layoutPie();
+ fadeIn();
+ } else {
+ mState = STATE_IDLE;
+ mTapMode = false;
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ }
+ setVisible(show);
+ mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+ }
+
+ private void fadeIn() {
+ mFadeIn = new LinearAnimation(0, 1);
+ mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+ mFadeIn.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mFadeIn = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mFadeIn.startNow();
+ mOverlay.startAnimation(mFadeIn);
+ }
+
+ public void setCenter(int x, int y) {
+ mCenter.x = x;
+ mCenter.y = y;
+ // when using the pie menu, align the focus ring
+ alignFocus(x, y);
+ }
+
+ private void layoutPie() {
+ int rgap = 2;
+ int inner = mRadius + rgap;
+ int outer = mRadius + mRadiusInc - rgap;
+ int gap = 1;
+ layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
+ }
+
+ private void layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap) {
+ float emptyangle = PIE_SWEEP / 16;
+ float sweep = (PIE_SWEEP - 2 * emptyangle) / items.size();
+ float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
+ // check if we have custom geometry
+ // first item we find triggers custom sweep for all
+ // this allows us to re-use the path
+ for (PieItem item : items) {
+ if (item.getCenter() >= 0) {
+ sweep = item.getSweep();
+ break;
+ }
+ }
+ Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
+ for (PieItem item : items) {
+ // shared between items
+ item.setPath(path);
+ if (item.getCenter() >= 0) {
+ angle = item.getCenter();
+ }
+ int w = item.getIntrinsicWidth();
+ int h = item.getIntrinsicHeight();
+ // move views to outer border
+ int r = inner + (outer - inner) * 2 / 3;
+ int x = (int) (r * Math.cos(angle));
+ int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
+ x = mCenter.x + x - w / 2;
+ item.setBounds(x, y, x + w, y + h);
+ float itemstart = angle - sweep / 2;
+ item.setGeometry(itemstart, sweep, inner, outer);
+ if (item.hasItems()) {
+ layoutItems(item.getItems(), angle, inner, outer + mRadiusInc / 2, gap);
+ }
+ angle += sweep;
+ }
+ }
+
+ private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+ RectF bb = new RectF(center.x - outer, center.y - outer, center.x + outer, center.y + outer);
+ RectF bbi = new RectF(center.x - inner, center.y - inner, center.x + inner, center.y + inner);
+ Path path = new Path();
+ path.arcTo(bb, start, end - start, true);
+ path.arcTo(bbi, end, start - end);
+ path.close();
+ return path;
+ }
+
+ /**
+ * converts a
+ *
+ * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+ * @return skia angle
+ */
+ private float getDegrees(double angle) {
+ return (float) (360 - 180 * angle / Math.PI);
+ }
+
+ private void startFadeOut() {
+ mOverlay
+ .animate()
+ .alpha(0)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ deselect();
+ show(false);
+ mOverlay.setAlpha(1);
+ super.onAnimationEnd(animation);
+ }
+ })
+ .setDuration(PIE_SELECT_FADE_DURATION);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ float alpha = 1;
+ if (mXFade != null) {
+ alpha = mXFade.getValue();
+ } else if (mFadeIn != null) {
+ alpha = mFadeIn.getValue();
+ }
+ int state = canvas.save();
+ if (mFadeIn != null) {
+ float sf = 0.9f + alpha * 0.1f;
+ canvas.scale(sf, sf, mCenter.x, mCenter.y);
+ }
+ drawFocus(canvas);
+ if (mState == STATE_FINISHING) {
+ canvas.restoreToCount(state);
+ return;
+ }
+ if ((mOpenItem == null) || (mXFade != null)) {
+ // draw base menu
+ for (PieItem item : mItems) {
+ drawItem(canvas, item, alpha);
+ }
+ }
+ if (mOpenItem != null) {
+ for (PieItem inner : mOpenItem.getItems()) {
+ drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+ }
+ }
+ canvas.restoreToCount(state);
+ }
+
+ private void drawItem(Canvas canvas, PieItem item, float alpha) {
+ if (mState == STATE_PIE) {
+ if (item.getPath() != null) {
+ if (item.isSelected()) {
+ Paint p = mSelectedPaint;
+ int state = canvas.save();
+ float r = getDegrees(item.getStartAngle());
+ canvas.rotate(r, mCenter.x, mCenter.y);
+ canvas.drawPath(item.getPath(), p);
+ canvas.restoreToCount(state);
+ }
+ alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+ // draw the item view
+ item.setAlpha(alpha);
+ item.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ float x = evt.getX();
+ float y = evt.getY();
+ int action = evt.getActionMasked();
+ PointF polar = getPolar(x, y, !(mTapMode));
+ if (MotionEvent.ACTION_DOWN == action) {
+ mDown.x = (int) evt.getX();
+ mDown.y = (int) evt.getY();
+ mOpening = false;
+ if (mTapMode) {
+ PieItem item = findItem(polar);
+ if ((item != null) && (mCurrentItem != item)) {
+ mState = STATE_PIE;
+ onEnter(item);
+ }
+ } else {
+ setCenter((int) x, (int) y);
+ show(true);
+ }
+ return true;
+ } else if (MotionEvent.ACTION_UP == action) {
+ if (isVisible()) {
+ PieItem item = mCurrentItem;
+ if (mTapMode) {
+ item = findItem(polar);
+ if (item != null && mOpening) {
+ mOpening = false;
+ return true;
+ }
+ }
+ if (item == null) {
+ mTapMode = false;
+ show(false);
+ } else if (!mOpening && !item.hasItems()) {
+ item.performClick();
+ startFadeOut();
+ mTapMode = false;
+ }
+ return true;
+ }
+ } else if (MotionEvent.ACTION_CANCEL == action) {
+ if (isVisible() || mTapMode) {
+ show(false);
+ }
+ deselect();
+ return false;
+ } else if (MotionEvent.ACTION_MOVE == action) {
+ if (polar.y < mRadius) {
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ } else {
+ deselect();
+ }
+ return false;
+ }
+ PieItem item = findItem(polar);
+ boolean moved = hasMoved(evt);
+ if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+ // only select if we didn't just open or have moved past slop
+ mOpening = false;
+ if (moved) {
+ // switch back to swipe mode
+ mTapMode = false;
+ }
+ onEnter(item);
+ }
+ }
+ return false;
+ }
+
+ private boolean hasMoved(MotionEvent e) {
+ return mTouchSlopSquared
+ < (e.getX() - mDown.x) * (e.getX() - mDown.x) + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+ }
+
+ /**
+ * enter a slice for a view updates model only
+ *
+ * @param item
+ */
+ private void onEnter(PieItem item) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null && item.isEnabled()) {
+ item.setSelected(true);
+ mCurrentItem = item;
+ if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+ openCurrentItem();
+ }
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private void deselect() {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ }
+ mCurrentItem = null;
+ }
+
+ private void openCurrentItem() {
+ if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+ mCurrentItem.setSelected(false);
+ mOpenItem = mCurrentItem;
+ mOpening = true;
+ mXFade = new LinearAnimation(1, 0);
+ mXFade.setDuration(PIE_XFADE_DURATION);
+ mXFade.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mXFade = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mXFade.startNow();
+ mOverlay.startAnimation(mXFade);
+ }
+ }
+
+ private PointF getPolar(float x, float y, boolean useOffset) {
+ PointF res = new PointF();
+ // get angle and radius from x/y
+ res.x = (float) Math.PI / 2;
+ x = x - mCenter.x;
+ y = mCenter.y - y;
+ res.y = (float) Math.sqrt(x * x + y * y);
+ if (x != 0) {
+ res.x = (float) Math.atan2(y, x);
+ if (res.x < 0) {
+ res.x = (float) (2 * Math.PI + res.x);
+ }
+ }
+ res.y = res.y + (useOffset ? mTouchOffset : 0);
+ return res;
+ }
+
+ /**
+ * @param polar x: angle, y: dist
+ * @return the item at angle/dist or null
+ */
+ private PieItem findItem(PointF polar) {
+ // find the matching item:
+ List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
+ for (PieItem item : items) {
+ if (inside(polar, item)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private boolean inside(PointF polar, PieItem item) {
+ return (item.getInnerRadius() < polar.y)
+ && (item.getStartAngle() < polar.x)
+ && (item.getStartAngle() + item.getSweep() > polar.x)
+ && (!mTapMode || (item.getOuterRadius() > polar.y));
+ }
+
+ @Override
+ public boolean handlesTouch() {
+ return true;
+ }
+
+ // focus specific code
+
+ public void setBlockFocus(boolean blocked) {
+ mBlockFocus = blocked;
+ if (blocked) {
+ clear();
+ }
+ }
+
+ public void setFocus(int x, int y) {
+ mFocusX = x;
+ mFocusY = y;
+ setCircle(mFocusX, mFocusY);
+ }
+
+ public void alignFocus(int x, int y) {
+ mOverlay.removeCallbacks(mDisappear);
+ mAnimation.cancel();
+ mAnimation.reset();
+ mFocusX = x;
+ mFocusY = y;
+ mDialAngle = DIAL_HORIZONTAL;
+ setCircle(x, y);
+ mFocused = false;
+ }
+
+ public int getSize() {
+ return 2 * mCircleSize;
+ }
+
+ private int getRandomRange() {
+ return (int) (-60 + 120 * Math.random());
+ }
+
+ @Override
+ public void layout(int l, int t, int r, int b) {
+ super.layout(l, t, r, b);
+ mCenterX = (r - l) / 2;
+ mCenterY = (b - t) / 2;
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ setCircle(mFocusX, mFocusY);
+ if (isVisible() && mState == STATE_PIE) {
+ setCenter(mCenterX, mCenterY);
+ layoutPie();
+ }
+ }
+
+ private void setCircle(int cx, int cy) {
+ mCircle.set(cx - mCircleSize, cy - mCircleSize, cx + mCircleSize, cy + mCircleSize);
+ mDial.set(
+ cx - mCircleSize + mInnerOffset,
+ cy - mCircleSize + mInnerOffset,
+ cx + mCircleSize - mInnerOffset,
+ cy + mCircleSize - mInnerOffset);
+ }
+
+ public void drawFocus(Canvas canvas) {
+ if (mBlockFocus) {
+ return;
+ }
+ mFocusPaint.setStrokeWidth(mOuterStroke);
+ canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
+ if (mState == STATE_PIE) {
+ return;
+ }
+ int color = mFocusPaint.getColor();
+ if (mState == STATE_FINISHING) {
+ mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
+ }
+ mFocusPaint.setStrokeWidth(mInnerStroke);
+ drawLine(canvas, mDialAngle, mFocusPaint);
+ drawLine(canvas, mDialAngle + 45, mFocusPaint);
+ drawLine(canvas, mDialAngle + 180, mFocusPaint);
+ drawLine(canvas, mDialAngle + 225, mFocusPaint);
+ canvas.save();
+ // rotate the arc instead of its offset to better use framework's shape caching
+ canvas.rotate(mDialAngle, mFocusX, mFocusY);
+ canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
+ canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
+ canvas.restore();
+ mFocusPaint.setColor(color);
+ }
+
+ private void drawLine(Canvas canvas, int angle, Paint p) {
+ convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
+ convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
+ canvas.drawLine(
+ mPoint1.x + mFocusX, mPoint1.y + mFocusY, mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
+ }
+
+ private static void convertCart(int angle, int radius, Point out) {
+ double a = 2 * Math.PI * (angle % 360) / 360;
+ out.x = (int) (radius * Math.cos(a) + 0.5);
+ out.y = (int) (radius * Math.sin(a) + 0.5);
+ }
+
+ @Override
+ public void showStart() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mStartAnimationAngle = 67;
+ int range = getRandomRange();
+ startAnimation(SCALING_UP_TIME, false, mStartAnimationAngle, mStartAnimationAngle + range);
+ mState = STATE_FOCUSING;
+ }
+
+ @Override
+ public void showSuccess(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = true;
+ }
+ }
+
+ @Override
+ public void showFail(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = false;
+ }
+ }
+
+ private void cancelFocus() {
+ mFocusCancelled = true;
+ mOverlay.removeCallbacks(mDisappear);
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ }
+ mFocusCancelled = false;
+ mFocused = false;
+ mState = STATE_IDLE;
+ }
+
+ @Override
+ public void clear() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mOverlay.post(mDisappear);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float toScale) {
+ startAnimation(duration, timeout, mDialAngle, toScale);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float fromScale, float toScale) {
+ setVisible(true);
+ mAnimation.reset();
+ mAnimation.setDuration(duration);
+ mAnimation.setScale(fromScale, toScale);
+ mAnimation.setAnimationListener(timeout ? mEndAction : null);
+ mOverlay.startAnimation(mAnimation);
+ update();
+ }
+
+ private class EndAction implements Animation.AnimationListener {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Keep the focus indicator for some time.
+ if (!mFocusCancelled) {
+ mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ }
+
+ private class Disappear implements Runnable {
+ @Override
+ public void run() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ setVisible(false);
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ mState = STATE_IDLE;
+ setCircle(mFocusX, mFocusY);
+ mFocused = false;
+ }
+ }
+
+ private class ScaleAnimation extends Animation {
+ private float mFrom = 1f;
+ private float mTo = 1f;
+
+ public ScaleAnimation() {
+ setFillAfter(true);
+ }
+
+ public void setScale(float from, float to) {
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mDialAngle = (int) (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+
+ private static class LinearAnimation extends Animation {
+ private float mFrom;
+ private float mTo;
+ private float mValue;
+
+ public LinearAnimation(float from, float to) {
+ setFillAfter(true);
+ setInterpolator(new LinearInterpolator());
+ mFrom = from;
+ mTo = to;
+ }
+
+ public float getValue() {
+ return mValue;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
new file mode 100644
index 000000000..c38ae6c81
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.camerafocus;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Focusing overlay. */
+public class RenderOverlay extends FrameLayout {
+
+ /** Render interface. */
+ interface Renderer {
+ boolean handlesTouch();
+
+ boolean onTouchEvent(MotionEvent evt);
+
+ void setOverlay(RenderOverlay overlay);
+
+ void layout(int left, int top, int right, int bottom);
+
+ void draw(Canvas canvas);
+ }
+
+ private RenderView mRenderView;
+ private List<Renderer> mClients;
+
+ // reverse list of touch clients
+ private List<Renderer> mTouchClients;
+ private int[] mPosition = new int[2];
+
+ public RenderOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRenderView = new RenderView(context);
+ addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ mClients = new ArrayList<>(10);
+ mTouchClients = new ArrayList<>(10);
+ setWillNotDraw(false);
+
+ addRenderer(new PieRenderer(context));
+ }
+
+ public PieRenderer getPieRenderer() {
+ for (Renderer renderer : mClients) {
+ if (renderer instanceof PieRenderer) {
+ return (PieRenderer) renderer;
+ }
+ }
+ return null;
+ }
+
+ public void addRenderer(Renderer renderer) {
+ mClients.add(renderer);
+ renderer.setOverlay(this);
+ if (renderer.handlesTouch()) {
+ mTouchClients.add(0, renderer);
+ }
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void addRenderer(int pos, Renderer renderer) {
+ mClients.add(pos, renderer);
+ renderer.setOverlay(this);
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void remove(Renderer renderer) {
+ mClients.remove(renderer);
+ renderer.setOverlay(null);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ return false;
+ }
+
+ private void adjustPosition() {
+ getLocationInWindow(mPosition);
+ }
+
+ public void update() {
+ mRenderView.invalidate();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private class RenderView extends View {
+
+ public RenderView(Context context) {
+ super(context);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ if (mTouchClients != null) {
+ boolean res = false;
+ for (Renderer client : mTouchClients) {
+ res |= client.onTouchEvent(evt);
+ }
+ return res;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ adjustPosition();
+ super.onLayout(changed, left, top, right, bottom);
+ if (mClients == null) {
+ return;
+ }
+ for (Renderer renderer : mClients) {
+ renderer.layout(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mClients == null) {
+ return;
+ }
+ boolean redraw = false;
+ for (Renderer renderer : mClients) {
+ renderer.draw(canvas);
+ redraw = redraw || ((OverlayRenderer) renderer).isVisible();
+ }
+ if (redraw) {
+ invalidate();
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
new file mode 100644
index 000000000..fba631b0e
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <!-- Camera focus indicator values -->
+ <dimen name="pie_radius_start">40dp</dimen>
+ <dimen name="pie_radius_increment">30dp</dimen>
+ <dimen name="pie_touch_offset">20dp</dimen>
+ <dimen name="focus_radius_offset">8dp</dimen>
+ <dimen name="focus_inner_offset">12dp</dimen>
+ <dimen name="focus_outer_stroke">3dp</dimen>
+ <dimen name="focus_inner_stroke">2dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
new file mode 100644
index 000000000..e2c8185da
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import com.android.dialer.common.Assert;
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class CountedDataInputStream extends FilterInputStream {
+
+ private int mCount = 0;
+
+ // allocate a byte buffer for a long value;
+ private final byte[] mByteArray = new byte[8];
+ private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+ CountedDataInputStream(InputStream in) {
+ super(in);
+ }
+
+ int getReadByteCount() {
+ return mCount;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int r = in.read(b);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int r = in.read(b, off, len);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int r = in.read();
+ mCount += (r >= 0) ? 1 : 0;
+ return r;
+ }
+
+ @Override
+ public long skip(long length) throws IOException {
+ long skip = in.skip(length);
+ mCount += skip;
+ return skip;
+ }
+
+ private void skipOrThrow(long length) throws IOException {
+ if (skip(length) != length) {
+ throw new EOFException();
+ }
+ }
+
+ void skipTo(long target) throws IOException {
+ long cur = mCount;
+ long diff = target - cur;
+ Assert.checkArgument(diff >= 0);
+ skipOrThrow(diff);
+ }
+
+ private void readOrThrow(byte[] b, int off, int len) throws IOException {
+ int r = read(b, off, len);
+ if (r != len) {
+ throw new EOFException();
+ }
+ }
+
+ private void readOrThrow(byte[] b) throws IOException {
+ readOrThrow(b, 0, b.length);
+ }
+
+ void setByteOrder(ByteOrder order) {
+ mByteBuffer.order(order);
+ }
+
+ ByteOrder getByteOrder() {
+ return mByteBuffer.order();
+ }
+
+ short readShort() throws IOException {
+ readOrThrow(mByteArray, 0, 2);
+ mByteBuffer.rewind();
+ return mByteBuffer.getShort();
+ }
+
+ int readUnsignedShort() throws IOException {
+ return readShort() & 0xffff;
+ }
+
+ int readInt() throws IOException {
+ readOrThrow(mByteArray, 0, 4);
+ mByteBuffer.rewind();
+ return mByteBuffer.getInt();
+ }
+
+ long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ String readString(int n, Charset charset) throws IOException {
+ byte[] buf = new byte[n];
+ readOrThrow(buf);
+ return new String(buf, charset);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifData.java b/java/com/android/dialer/callcomposer/camera/exif/ExifData.java
new file mode 100644
index 000000000..27936ae2f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifData.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG specification. It is the result
+ * produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
+ */
+public class ExifData {
+
+ private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
+
+ /**
+ * Adds IFD data. If IFD data of the same type already exists, it will be replaced by the new
+ * data.
+ */
+ void addIfdData(IfdData data) {
+ mIfdDatas[data.getId()] = data;
+ }
+
+ /** Returns the {@link IfdData} object corresponding to a given IFD if it exists or null. */
+ IfdData getIfdData(int ifdId) {
+ if (ExifTag.isValidIfd(ifdId)) {
+ return mIfdDatas[ifdId];
+ }
+ return null;
+ }
+
+ /**
+ * Returns the tag with a given TID in the given IFD if the tag exists. Otherwise returns null.
+ */
+ protected ExifTag getTag(short tag, int ifd) {
+ IfdData ifdData = mIfdDatas[ifd];
+ return (ifdData == null) ? null : ifdData.getTag(tag);
+ }
+
+ /**
+ * Adds the given ExifTag to its default IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ ExifTag addTag(ExifTag tag) {
+ if (tag != null) {
+ int ifd = tag.getIfd();
+ return addTag(tag, ifd);
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given ExifTag to the given IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ private ExifTag addTag(ExifTag tag, int ifdId) {
+ if (tag != null && ExifTag.isValidIfd(ifdId)) {
+ IfdData ifdData = getOrCreateIfdData(ifdId);
+ return ifdData.setTag(tag);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IfdData} object corresponding to a given IFD or generates one if none exist.
+ */
+ private IfdData getOrCreateIfdData(int ifdId) {
+ IfdData ifdData = mIfdDatas[ifdId];
+ if (ifdData == null) {
+ ifdData = new IfdData(ifdId);
+ mIfdDatas[ifdId] = ifdData;
+ }
+ return ifdData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
new file mode 100644
index 000000000..92dee1c94
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.util.SparseIntArray;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.HashSet;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file metadata. It contains
+ * a collection of ExifTags, and a collection of definitions for creating valid ExifTags. The
+ * collection of ExifTags can be updated by: reading new ones from a file, deleting or adding
+ * existing ones, or building new ExifTags from a tag definition. These ExifTags can be written to a
+ * valid jpeg image as exif metadata.
+ *
+ * <p>Each ExifTag has a tag ID (TID) and is stored in a specific image file directory (IFD) as
+ * specified by the exif standard. A tag definition can be looked up with a constant that is a
+ * combination of TID and IFD. This definition has information about the type, number of components,
+ * and valid IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+ private static final int IFD_NULL = -1;
+ static final int DEFINITION_NULL = 0;
+
+ /** Tag constants for Jeita EXIF 2.2 */
+ // IFD 0
+ public static final int TAG_ORIENTATION = defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+
+ static final int TAG_EXIF_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+ static final int TAG_GPS_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+ static final int TAG_STRIP_OFFSETS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+ static final int TAG_STRIP_BYTE_COUNTS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+ // IFD 1
+ static final int TAG_JPEG_INTERCHANGE_FORMAT = defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+ static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+ // IFD Exif Tags
+ static final int TAG_INTEROPERABILITY_IFD = defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+
+ /** Tags that contain offset markers. These are included in the banned defines. */
+ private static HashSet<Short> sOffsetTags = new HashSet<>();
+
+ static {
+ sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+ sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+ }
+
+ private static final String NULL_ARGUMENT_STRING = "Argument is null";
+
+ private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+
+ private ExifData mData = new ExifData();
+
+ @SuppressLint("SimpleDateFormat")
+ public ExifInterface() {
+ DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+ mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Reads the exif tags from a byte array, clearing this ExifInterface object's existing exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ public void readExif(byte[] jpeg) throws IOException {
+ readExif(new ByteArrayInputStream(jpeg));
+ }
+
+ /**
+ * Reads the exif tags from an InputStream, clearing this ExifInterface object's existing exif
+ * tags.
+ *
+ * @param inStream an InputStream containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ private void readExif(InputStream inStream) throws IOException {
+ if (inStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ ExifData d;
+ try {
+ d = new ExifReader(this).read(inStream);
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : " + e);
+ }
+ mData = d;
+ }
+
+ /** Returns the TID for a tag constant. */
+ static short getTrueTagKey(int tag) {
+ // Truncate
+ return (short) tag;
+ }
+
+ /** Returns the constant representing a tag with a given TID and default IFD. */
+ private static int defineTag(int ifdId, short tagId) {
+ return (tagId & 0x0000ffff) | (ifdId << 16);
+ }
+
+ static boolean isIfdAllowed(int info, int ifd) {
+ int[] ifds = IfdData.getIfds();
+ int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+ for (int i = 0; i < ifds.length; i++) {
+ if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int getAllowedIfdFlagsFromInfo(int info) {
+ return info >>> 24;
+ }
+
+ /**
+ * Returns true if tag TID is one of the following: {@code TAG_EXIF_IFD}, {@code TAG_GPS_IFD},
+ * {@code TAG_JPEG_INTERCHANGE_FORMAT}, {@code TAG_STRIP_OFFSETS}, {@code
+ * TAG_INTEROPERABILITY_IFD}
+ *
+ * <p>Note: defining tags with these TID's is disallowed.
+ *
+ * @param tag a tag's TID (can be obtained from a defined tag constant with {@link
+ * #getTrueTagKey}).
+ * @return true if the TID is that of an offset tag.
+ */
+ static boolean isOffsetTag(short tag) {
+ return sOffsetTags.contains(tag);
+ }
+
+ private SparseIntArray mTagInfo = null;
+
+ SparseIntArray getTagInfo() {
+ if (mTagInfo == null) {
+ mTagInfo = new SparseIntArray();
+ initTagInfo();
+ }
+ return mTagInfo;
+ }
+
+ private void initTagInfo() {
+ /**
+ * We put tag information in a 4-bytes integer. The first byte a bitmask representing the
+ * allowed IFDs of the tag, the second byte is the data type, and the last two byte are a short
+ * value indicating the default component count of this tag.
+ */
+ // IFD0 tags
+ int[] ifdAllowedIfds = {IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1};
+ int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ mTagInfo.put(ExifInterface.TAG_EXIF_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ // IFD1 tags
+ int[] ifd1AllowedIfds = {IfdId.TYPE_IFD_1};
+ int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // Exif tags
+ int[] exifAllowedIfds = {IfdId.TYPE_IFD_EXIF};
+ int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ }
+
+ private static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+ if (allowedIfds == null || allowedIfds.length == 0) {
+ return 0;
+ }
+ int flags = 0;
+ int[] ifds = IfdData.getIfds();
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ for (int j : allowedIfds) {
+ if (ifds[i] == j) {
+ flags |= 1 << i;
+ break;
+ }
+ }
+ }
+ return flags;
+ }
+
+ private Integer getTagIntValue(int tagId, int ifdId) {
+ int[] l = getTagIntValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return l[0];
+ }
+
+ private int[] getTagIntValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsInts();
+ }
+
+ /** Gets an ExifTag for an IFD other than the tag's default. */
+ public ExifTag getTag(int tagId, int ifdId) {
+ if (!ExifTag.isValidIfd(ifdId)) {
+ return null;
+ }
+ return mData.getTag(getTrueTagKey(tagId), ifdId);
+ }
+
+ public Integer getTagIntValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagIntValue(tagId, ifdId);
+ }
+
+ /**
+ * Gets the default IFD for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return the default IFD for a tag definition or {@link #IFD_NULL} if no definition exists.
+ */
+ private int getDefinedTagDefaultIfd(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == DEFINITION_NULL) {
+ return IFD_NULL;
+ }
+ return getTrueIfd(tagId);
+ }
+
+ /** Returns the default IFD for a tag constant. */
+ private static int getTrueIfd(int tag) {
+ return tag >>> 16;
+ }
+
+ /**
+ * Constants for {@code TAG_ORIENTATION}. They can be interpreted as follows:
+ *
+ * <ul>
+ * <li>TOP_LEFT is the normal orientation.
+ * <li>TOP_RIGHT is a left-right mirror.
+ * <li>BOTTOM_LEFT is a 180 degree rotation.
+ * <li>BOTTOM_RIGHT is a top-bottom mirror.
+ * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.
+ * <li>RIGHT_TOP is a 90 degree clockwise rotation.
+ * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.
+ * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.
+ * </ul>
+ */
+ interface Orientation {
+ short TOP_LEFT = 1;
+ short TOP_RIGHT = 2;
+ short BOTTOM_LEFT = 3;
+ short BOTTOM_RIGHT = 4;
+ short LEFT_TOP = 5;
+ short RIGHT_TOP = 6;
+ short LEFT_BOTTOM = 7;
+ short RIGHT_BOTTOM = 8;
+ }
+
+ /** Wrapper class to define some orientation parameters. */
+ public static class OrientationParams {
+ int rotation = 0;
+ int scaleX = 1;
+ int scaleY = 1;
+ public boolean invertDimensions = false;
+ }
+
+ public static OrientationParams getOrientationParams(int orientation) {
+ OrientationParams params = new OrientationParams();
+ switch (orientation) {
+ case Orientation.TOP_RIGHT: // Flip horizontal
+ params.scaleX = -1;
+ break;
+ case Orientation.BOTTOM_RIGHT: // Flip vertical
+ params.scaleY = -1;
+ break;
+ case Orientation.BOTTOM_LEFT: // Rotate 180
+ params.rotation = 180;
+ break;
+ case Orientation.RIGHT_BOTTOM: // Rotate 270
+ params.rotation = 270;
+ params.invertDimensions = true;
+ break;
+ case Orientation.RIGHT_TOP: // Rotate 90
+ params.rotation = 90;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_TOP: // Transpose
+ params.rotation = 90;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_BOTTOM: // Transverse
+ params.rotation = 270;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ }
+ return params;
+ }
+
+ /** Clears this ExifInterface object's existing exif tags. */
+ public void clearExif() {
+ mData = new ExifData();
+ }
+
+ /**
+ * Puts an ExifTag into this ExifInterface object's tags, removing a previous ExifTag with the
+ * same TID and IFD. The IFD it is put into will be the one the tag was created with in {@link
+ * #buildTag}.
+ *
+ * @param tag an ExifTag to put into this ExifInterface's tags.
+ * @return the previous ExifTag with the same TID and IFD or null if none exists.
+ */
+ public ExifTag setTag(ExifTag tag) {
+ return mData.addTag(tag);
+ }
+
+ /**
+ * Returns the ExifTag in that tag's default IFD for a defined tag constant or null if none
+ * exists.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return an {@link ExifTag} or null if none exists.
+ */
+ public ExifTag getTag(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTag(tagId, ifdId);
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg compressed bitmap, removing prior
+ * exif tags.
+ *
+ * @param bmap a bitmap to compress and write exif into.
+ * @param exifOutStream the OutputStream to which the jpeg image with added exif tags will be
+ * written.
+ * @throws java.io.IOException
+ */
+ public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+ if (bmap == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ bmap.compress(Bitmap.CompressFormat.JPEG, 90, exifOutStream);
+ exifOutStream.flush();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
new file mode 100644
index 000000000..92449d74f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+/** Exception for invalid exif formats. */
+public class ExifInvalidFormatException extends Exception {
+ ExifInvalidFormatException(String meg) {
+ super(meg);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
new file mode 100644
index 000000000..23d748c17
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
@@ -0,0 +1,846 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class provides a low-level EXIF parsing API. Given a JPEG format InputStream, the caller can
+ * request which IFD's to read via {@link #parse(java.io.InputStream, int)} with given options.
+ *
+ * <p>Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the parser.
+ *
+ * <pre>
+ * void parse() {
+ * ExifParser parser = ExifParser.parse(mImageInputStream,
+ * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ * int event = parser.next();
+ * while (event != ExifParser.EVENT_END) {
+ * switch (event) {
+ * case ExifParser.EVENT_START_OF_IFD:
+ * break;
+ * case ExifParser.EVENT_NEW_TAG:
+ * ExifTag tag = parser.getTag();
+ * if (!tag.hasValue()) {
+ * parser.registerForTagValue(tag);
+ * } else {
+ * processTag(tag);
+ * }
+ * break;
+ * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ * tag = parser.getTag();
+ * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ * processTag(tag);
+ * }
+ * break;
+ * }
+ * event = parser.next();
+ * }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ * // process the tag as you like.
+ * }
+ * </pre>
+ */
+public class ExifParser {
+ private static final boolean LOGV = false;
+ /**
+ * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to know which IFD we are
+ * in.
+ */
+ static final int EVENT_START_OF_IFD = 0;
+ /** When the parser reaches a new tag. Call {@link #getTag()}to get the corresponding tag. */
+ static final int EVENT_NEW_TAG = 1;
+ /**
+ * When the parser reaches the value area of tag that is registered by {@link
+ * #registerForTagValue(ExifTag)} previously. Call {@link #getTag()} to get the corresponding tag.
+ */
+ static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
+
+ /** When the parser reaches the compressed image area. */
+ static final int EVENT_COMPRESSED_IMAGE = 3;
+ /**
+ * When the parser reaches the uncompressed image strip. Call {@link #getStripIndex()} to get the
+ * index of the strip.
+ *
+ * @see #getStripIndex()
+ */
+ static final int EVENT_UNCOMPRESSED_STRIP = 4;
+ /** When there is nothing more to parse. */
+ static final int EVENT_END = 5;
+
+ /** Option bit to request to parse IFD0. */
+ private static final int OPTION_IFD_0 = 1;
+ /** Option bit to request to parse IFD1. */
+ private static final int OPTION_IFD_1 = 1 << 1;
+ /** Option bit to request to parse Exif-IFD. */
+ private static final int OPTION_IFD_EXIF = 1 << 2;
+ /** Option bit to request to parse GPS-IFD. */
+ private static final int OPTION_IFD_GPS = 1 << 3;
+ /** Option bit to request to parse Interoperability-IFD. */
+ private static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
+ /** Option bit to request to parse thumbnail. */
+ private static final int OPTION_THUMBNAIL = 1 << 5;
+
+ private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+ private static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+
+ // TIFF header
+ private static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+ private static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+ private static final short TIFF_HEADER_TAIL = 0x002A;
+
+ private static final int TAG_SIZE = 12;
+ private static final int OFFSET_SIZE = 2;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+ private static final int DEFAULT_IFD0_OFFSET = 8;
+
+ private final CountedDataInputStream mTiffStream;
+ private final int mOptions;
+ private int mIfdStartOffset = 0;
+ private int mNumOfTagInIfd = 0;
+ private int mIfdType;
+ private ExifTag mTag;
+ private ImageEvent mImageEvent;
+ private ExifTag mStripSizeTag;
+ private ExifTag mJpegSizeTag;
+ private boolean mNeedToParseOffsetsInCurrentIfd;
+ private boolean mContainExifData = false;
+ private int mApp1End;
+ private byte[] mDataAboveIfd0;
+ private int mIfd0Position;
+ private final ExifInterface mInterface;
+
+ private static final short TAG_EXIF_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+ private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+ private static final short TAG_INTEROPERABILITY_IFD =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ private static final short TAG_STRIP_OFFSETS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+ private static final short TAG_STRIP_BYTE_COUNTS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+
+ private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<>();
+
+ private boolean isIfdRequested(int ifdType) {
+ switch (ifdType) {
+ case IfdId.TYPE_IFD_0:
+ return (mOptions & OPTION_IFD_0) != 0;
+ case IfdId.TYPE_IFD_1:
+ return (mOptions & OPTION_IFD_1) != 0;
+ case IfdId.TYPE_IFD_EXIF:
+ return (mOptions & OPTION_IFD_EXIF) != 0;
+ case IfdId.TYPE_IFD_GPS:
+ return (mOptions & OPTION_IFD_GPS) != 0;
+ case IfdId.TYPE_IFD_INTEROPERABILITY:
+ return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
+ }
+ return false;
+ }
+
+ private boolean isThumbnailRequested() {
+ return (mOptions & OPTION_THUMBNAIL) != 0;
+ }
+
+ private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ if (inputStream == null) {
+ throw new IOException("Null argument inputStream to ExifParser");
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.ExifParser", "Reading exif...");
+ }
+ mInterface = iRef;
+ mContainExifData = seekTiffData(inputStream);
+ mTiffStream = new CountedDataInputStream(inputStream);
+ mOptions = options;
+ if (!mContainExifData) {
+ return;
+ }
+
+ parseTiffHeader();
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Invalid offset " + offset);
+ }
+ mIfd0Position = (int) offset;
+ mIfdType = IfdId.TYPE_IFD_0;
+ if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
+ registerIfd(IfdId.TYPE_IFD_0, offset);
+ if (offset != DEFAULT_IFD0_OFFSET) {
+ mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
+ read(mDataAboveIfd0);
+ }
+ }
+ }
+
+ /**
+ * Parses the the given InputStream with the given options
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ */
+ protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(inputStream, options, iRef);
+ }
+
+ /**
+ * Parses the the given InputStream with default options; that is, every IFD and thumbnaill will
+ * be parsed.
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #parse(java.io.InputStream, int, ExifInterface)
+ */
+ protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(
+ inputStream,
+ OPTION_IFD_0
+ | OPTION_IFD_1
+ | OPTION_IFD_EXIF
+ | OPTION_IFD_GPS
+ | OPTION_IFD_INTEROPERABILITY
+ | OPTION_THUMBNAIL,
+ iRef);
+ }
+
+ /**
+ * Moves the parser forward and returns the next parsing event
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #EVENT_START_OF_IFD
+ * @see #EVENT_NEW_TAG
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ * @see #EVENT_COMPRESSED_IMAGE
+ * @see #EVENT_UNCOMPRESSED_STRIP
+ * @see #EVENT_END
+ */
+ protected int next() throws IOException, ExifInvalidFormatException {
+ if (!mContainExifData) {
+ return EVENT_END;
+ }
+ int offset = mTiffStream.getReadByteCount();
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ if (offset < endOfTags) {
+ mTag = readTag();
+ if (mTag == null) {
+ return next();
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ checkOffsetOrImageTag(mTag);
+ }
+ return EVENT_NEW_TAG;
+ } else if (offset == endOfTags) {
+ // There is a link to ifd1 at the end of ifd0
+ if (mIfdType == IfdId.TYPE_IFD_0) {
+ long ifdOffset = readUnsignedLong();
+ if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
+ if (ifdOffset != 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ } else {
+ int offsetSize = 4;
+ // Some camera models use invalid length of the offset
+ if (mCorrespondingEvent.size() > 0) {
+ offsetSize = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ }
+ if (offsetSize < 4) {
+ LogUtil.i("ExifParser.next", "Invalid size of link to next IFD: " + offsetSize);
+ } else {
+ long ifdOffset = readUnsignedLong();
+ if (ifdOffset != 0) {
+ LogUtil.i("ExifParser.next", "Invalid link to next IFD: " + ifdOffset);
+ }
+ }
+ }
+ }
+ while (mCorrespondingEvent.size() != 0) {
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ Object event = entry.getValue();
+ try {
+ skipTo(entry.getKey());
+ } catch (IOException e) {
+ LogUtil.i(
+ "ExifParser.next",
+ "Failed to skip to data at: "
+ + entry.getKey()
+ + " for "
+ + event.getClass().getName()
+ + ", the file may be broken.");
+ continue;
+ }
+ if (event instanceof IfdEvent) {
+ mIfdType = ((IfdEvent) event).ifd;
+ mNumOfTagInIfd = mTiffStream.readUnsignedShort();
+ mIfdStartOffset = entry.getKey();
+
+ if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
+ LogUtil.i("ExifParser.next", "Invalid size of IFD " + mIfdType);
+ return EVENT_END;
+ }
+
+ mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
+ if (((IfdEvent) event).isRequested) {
+ return EVENT_START_OF_IFD;
+ } else {
+ skipRemainingTagsInCurrentIfd();
+ }
+ } else if (event instanceof ImageEvent) {
+ mImageEvent = (ImageEvent) event;
+ return mImageEvent.type;
+ } else {
+ ExifTagEvent tagEvent = (ExifTagEvent) event;
+ mTag = tagEvent.tag;
+ if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ readFullTagValue(mTag);
+ checkOffsetOrImageTag(mTag);
+ }
+ if (tagEvent.isRequested) {
+ return EVENT_VALUE_OF_REGISTERED_TAG;
+ }
+ }
+ }
+ return EVENT_END;
+ }
+
+ /**
+ * Skips the tags area of current IFD, if the parser is not in the tag area, nothing will happen.
+ *
+ * @throws java.io.IOException
+ * @throws ExifInvalidFormatException
+ */
+ private void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ int offset = mTiffStream.getReadByteCount();
+ if (offset > endOfTags) {
+ return;
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ while (offset < endOfTags) {
+ mTag = readTag();
+ offset += TAG_SIZE;
+ if (mTag == null) {
+ continue;
+ }
+ checkOffsetOrImageTag(mTag);
+ }
+ } else {
+ skipTo(endOfTags);
+ }
+ long ifdOffset = readUnsignedLong();
+ // For ifd0, there is a link to ifd1 in the end of all tags
+ if (mIfdType == IfdId.TYPE_IFD_0
+ && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
+ if (ifdOffset > 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ }
+
+ private boolean needToParseOffsetsInCurrentIfd() {
+ switch (mIfdType) {
+ case IfdId.TYPE_IFD_0:
+ return isIfdRequested(IfdId.TYPE_IFD_EXIF)
+ || isIfdRequested(IfdId.TYPE_IFD_GPS)
+ || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
+ || isIfdRequested(IfdId.TYPE_IFD_1);
+ case IfdId.TYPE_IFD_1:
+ return isThumbnailRequested();
+ case IfdId.TYPE_IFD_EXIF:
+ // The offset to interoperability IFD is located in Exif IFD
+ return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * If {@link #next()} return {@link #EVENT_NEW_TAG} or {@link #EVENT_VALUE_OF_REGISTERED_TAG},
+ * call this function to get the corresponding tag.
+ *
+ * <p>For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size of the value is
+ * greater than 4 bytes. One should call {@link ExifTag#hasValue()} to check if the tag contains
+ * value. If there is no value,call {@link #registerForTagValue(ExifTag)} to have the parser emit
+ * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area pointed by the offset.
+ *
+ * <p>When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the tag will have
+ * already been read except for tags of undefined type. For tags of undefined type, call one of
+ * the read methods to get the value.
+ *
+ * @see #registerForTagValue(ExifTag)
+ * @see #read(byte[])
+ * @see #read(byte[], int, int)
+ * @see #readLong()
+ * @see #readRational()
+ * @see #readString(int)
+ * @see #readString(int, java.nio.charset.Charset)
+ */
+ protected ExifTag getTag() {
+ return mTag;
+ }
+
+ /**
+ * Gets the ID of current IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ * @see IfdId#TYPE_IFD_EXIF
+ */
+ int getCurrentIfd() {
+ return mIfdType;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the index of this
+ * strip.
+ */
+ int getStripIndex() {
+ return mImageEvent.stripIndex;
+ }
+
+ /** When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the strip size. */
+ int getStripSize() {
+ if (mStripSizeTag == null) {
+ return 0;
+ }
+ return (int) mStripSizeTag.getValueAt(0);
+ }
+
+ /**
+ * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get the image data size.
+ */
+ int getCompressedImageSize() {
+ if (mJpegSizeTag == null) {
+ return 0;
+ }
+ return (int) mJpegSizeTag.getValueAt(0);
+ }
+
+ private void skipTo(int offset) throws IOException {
+ mTiffStream.skipTo(offset);
+ while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
+ mCorrespondingEvent.pollFirstEntry();
+ }
+ }
+
+ /**
+ * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may not contain the value
+ * if the size of the value is greater than 4 bytes. When the value is not available here, call
+ * this method so that the parser will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches
+ * the area where the value is located.
+ *
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ */
+ void registerForTagValue(ExifTag tag) {
+ if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
+ }
+ }
+
+ private void registerIfd(int ifdType, long offset) {
+ // Cast unsigned int to int since the offset is always smaller
+ // than the size of APP1 (65536)
+ mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
+ }
+
+ private void registerCompressedImage(long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
+ }
+
+ private void registerUncompressedStrip(int stripIndex, long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP, stripIndex));
+ }
+
+ @SuppressLint("DefaultLocale")
+ private ExifTag readTag() throws IOException, ExifInvalidFormatException {
+ short tagId = mTiffStream.readShort();
+ short dataFormat = mTiffStream.readShort();
+ long numOfComp = mTiffStream.readUnsignedInt();
+ if (numOfComp > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Number of component is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid image file contains invalid data type. Ignore those tags
+ if (!ExifTag.isValidType(dataFormat)) {
+ LogUtil.i("ExifParser.readTag", "Tag %04x: Invalid data type %d", tagId, dataFormat);
+ mTiffStream.skip(4);
+ return null;
+ }
+ // TODO: handle numOfComp overflow
+ ExifTag tag =
+ new ExifTag(
+ tagId,
+ dataFormat,
+ (int) numOfComp,
+ mIfdType,
+ ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
+ int dataSize = tag.getDataSize();
+ if (dataSize > 4) {
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("offset is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid images put some undefined data before IFD0.
+ // Read the data here.
+ if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
+ byte[] buf = new byte[(int) numOfComp];
+ System.arraycopy(
+ mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET, buf, 0, (int) numOfComp);
+ tag.setValue(buf);
+ } else {
+ tag.setOffset((int) offset);
+ }
+ } else {
+ boolean defCount = tag.hasDefinedCount();
+ // Set defined count to 0 so we can add \0 to non-terminated strings
+ tag.setHasDefinedCount(false);
+ // Read value
+ readFullTagValue(tag);
+ tag.setHasDefinedCount(defCount);
+ mTiffStream.skip(4 - dataSize);
+ // Set the offset to the position of value.
+ tag.setOffset(mTiffStream.getReadByteCount() - 4);
+ }
+ return tag;
+ }
+
+ /**
+ * Check the if the tag is one of the offset tag that points to the IFD or image the caller is
+ * interested in, register the IFD or image.
+ */
+ private void checkOffsetOrImageTag(ExifTag tag) {
+ // Some invalid formattd image contains tag with 0 size.
+ if (tag.getComponentCount() == 0) {
+ return;
+ }
+ short tid = tag.getTagId();
+ int ifd = tag.getIfd();
+ if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+ registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_INTEROPERABILITY_IFD
+ && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+ if (isThumbnailRequested()) {
+ registerCompressedImage(tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+ if (isThumbnailRequested()) {
+ mJpegSizeTag = tag;
+ }
+ } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+ if (isThumbnailRequested()) {
+ if (tag.hasValue()) {
+ for (int i = 0; i < tag.getComponentCount(); i++) {
+ if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ } else {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ }
+ }
+ } else {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
+ }
+ }
+ } else if (tid == TAG_STRIP_BYTE_COUNTS
+ && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+ && isThumbnailRequested()
+ && tag.hasValue()) {
+ mStripSizeTag = tag;
+ }
+ }
+
+ private boolean checkAllowed(int ifd, int tagId) {
+ int info = mInterface.getTagInfo().get(tagId);
+ return info != ExifInterface.DEFINITION_NULL && ExifInterface.isIfdAllowed(info, ifd);
+ }
+
+ void readFullTagValue(ExifTag tag) throws IOException {
+ // Some invalid images contains tags with wrong size, check it here
+ short type = tag.getDataType();
+ if (type == ExifTag.TYPE_ASCII
+ || type == ExifTag.TYPE_UNDEFINED
+ || type == ExifTag.TYPE_UNSIGNED_BYTE) {
+ int size = tag.getComponentCount();
+ if (mCorrespondingEvent.size() > 0) {
+ if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount() + size) {
+ Object event = mCorrespondingEvent.firstEntry().getValue();
+ if (event instanceof ImageEvent) {
+ // Tag value overlaps thumbnail, ignore thumbnail.
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Thumbnail overlaps value for tag: \n" + tag.toString());
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ LogUtil.i("ExifParser.readFullTagValue", "Invalid thumbnail offset: " + entry.getKey());
+ } else {
+ // Tag value overlaps another shorten count
+ if (event instanceof IfdEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Ifd " + ((IfdEvent) event).ifd + " overlaps value for tag: \n" + tag.toString());
+ } else if (event instanceof ExifTagEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Tag value for tag: \n"
+ + ((ExifTagEvent) event).tag.toString()
+ + " overlaps value for tag: \n"
+ + tag.toString());
+ }
+ size = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Invalid size of tag: \n" + tag.toString() + " setting count to: " + size);
+ tag.forceSetComponentCount(size);
+ }
+ }
+ }
+ }
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ case ExifTag.TYPE_UNDEFINED:
+ {
+ byte[] buf = new byte[tag.getComponentCount()];
+ read(buf);
+ tag.setValue(buf);
+ }
+ break;
+ case ExifTag.TYPE_ASCII:
+ tag.setValue(readString(tag.getComponentCount()));
+ break;
+ case ExifTag.TYPE_UNSIGNED_LONG:
+ {
+ long[] value = new long[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedShort();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_LONG:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.readFullTagValue", "\n" + tag.toString());
+ }
+ }
+
+ private void parseTiffHeader() throws IOException, ExifInvalidFormatException {
+ short byteOrder = mTiffStream.readShort();
+ if (LITTLE_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ } else if (BIG_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ } else {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+
+ if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+ }
+
+ private boolean seekTiffData(InputStream inputStream)
+ throws IOException, ExifInvalidFormatException {
+ CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
+ if (dataStream.readShort() != JpegHeader.SOI) {
+ throw new ExifInvalidFormatException("Invalid JPEG format");
+ }
+
+ short marker = dataStream.readShort();
+ while (marker != JpegHeader.EOI && !JpegHeader.isSofMarker(marker)) {
+ int length = dataStream.readUnsignedShort();
+ // Some invalid formatted image contains multiple APP1,
+ // try to find the one with Exif data.
+ if (marker == JpegHeader.APP1) {
+ int header;
+ short headerTail;
+ if (length >= 8) {
+ header = dataStream.readInt();
+ headerTail = dataStream.readShort();
+ length -= 6;
+ if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
+ mApp1End = length;
+ return true;
+ }
+ }
+ }
+ if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
+ LogUtil.i("ExifParser.seekTiffData", "Invalid JPEG format.");
+ return false;
+ }
+ marker = dataStream.readShort();
+ }
+ return false;
+ }
+
+ /** Reads bytes from the InputStream. */
+ protected int read(byte[] buffer, int offset, int length) throws IOException {
+ return mTiffStream.read(buffer, offset, length);
+ }
+
+ /** Equivalent to read(buffer, 0, buffer.length). */
+ protected int read(byte[] buffer) throws IOException {
+ return mTiffStream.read(buffer);
+ }
+
+ /**
+ * Reads a String from the InputStream with US-ASCII charset. The parser will read n bytes and
+ * convert it to ascii string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n) throws IOException {
+ return readString(n, US_ASCII);
+ }
+
+ /**
+ * Reads a String from the InputStream with the given charset. The parser will read n bytes and
+ * convert it to string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n, Charset charset) throws IOException {
+ if (n > 0) {
+ return mTiffStream.readString(n, charset);
+ } else {
+ return "";
+ }
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the InputStream. */
+ private int readUnsignedShort() throws IOException {
+ return mTiffStream.readShort() & 0xffff;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the InputStream. */
+ private long readUnsignedLong() throws IOException {
+ return readLong() & 0xffffffffL;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the InputStream. */
+ private Rational readUnsignedRational() throws IOException {
+ long nomi = readUnsignedLong();
+ long denomi = readUnsignedLong();
+ return new Rational(nomi, denomi);
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. */
+ private int readLong() throws IOException {
+ return mTiffStream.readInt();
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. */
+ private Rational readRational() throws IOException {
+ int nomi = readLong();
+ int denomi = readLong();
+ return new Rational(nomi, denomi);
+ }
+
+ private static class ImageEvent {
+ int stripIndex;
+ int type;
+
+ ImageEvent(int type) {
+ this.stripIndex = 0;
+ this.type = type;
+ }
+
+ ImageEvent(int type, int stripIndex) {
+ this.type = type;
+ this.stripIndex = stripIndex;
+ }
+ }
+
+ private static class IfdEvent {
+ int ifd;
+ boolean isRequested;
+
+ IfdEvent(int ifd, boolean isInterestedIfd) {
+ this.ifd = ifd;
+ this.isRequested = isInterestedIfd;
+ }
+ }
+
+ private static class ExifTagEvent {
+ ExifTag tag;
+ boolean isRequested;
+
+ ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
+ this.tag = tag;
+ this.isRequested = isRequireByUser;
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
new file mode 100644
index 000000000..89d212661
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** This class reads the EXIF header of a JPEG file and stores it in {@link ExifData}. */
+class ExifReader {
+
+ private final ExifInterface mInterface;
+
+ ExifReader(ExifInterface iRef) {
+ mInterface = iRef;
+ }
+
+ /**
+ * Parses the inputStream and and returns the EXIF data in an {@link ExifData}.
+ *
+ * @throws ExifInvalidFormatException
+ * @throws java.io.IOException
+ */
+ protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException, IOException {
+ ExifParser parser = ExifParser.parse(inputStream, mInterface);
+ ExifData exifData = new ExifData();
+ ExifTag tag;
+
+ int event = parser.next();
+ while (event != ExifParser.EVENT_END) {
+ switch (event) {
+ case ExifParser.EVENT_START_OF_IFD:
+ exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
+ break;
+ case ExifParser.EVENT_NEW_TAG:
+ tag = parser.getTag();
+ if (!tag.hasValue()) {
+ parser.registerForTagValue(tag);
+ } else {
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ }
+ break;
+ case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ tag = parser.getTag();
+ if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+ parser.readFullTagValue(tag);
+ }
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ break;
+ case ExifParser.EVENT_COMPRESSED_IMAGE:
+ byte[] buf = new byte[parser.getCompressedImageSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the compressed thumbnail");
+ }
+ break;
+ case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+ buf = new byte[parser.getStripSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the strip bytes");
+ }
+ break;
+ }
+ event = parser.next();
+ }
+ return exifData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
new file mode 100644
index 000000000..a254ae93b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class stores information of an EXIF tag. For more information about defined EXIF tags,
+ * please read the Jeita EXIF 2.2 standard. Tags should be instantiated using {@link
+ * ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
+ */
+public class ExifTag {
+ /** The BYTE type in the EXIF standard. An 8-bit unsigned integer. */
+ static final short TYPE_UNSIGNED_BYTE = 1;
+ /**
+ * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit ASCII code. The final
+ * byte is terminated with NULL.
+ */
+ static final short TYPE_ASCII = 2;
+ /** The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_SHORT = 3;
+ /** The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_LONG = 4;
+ /**
+ * The RATIONAL type of EXIF standard. It consists of two LONGs. The first one is the numerator
+ * and the second one expresses the denominator.
+ */
+ static final short TYPE_UNSIGNED_RATIONAL = 5;
+ /**
+ * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any value depending on the
+ * field definition.
+ */
+ static final short TYPE_UNDEFINED = 7;
+ /**
+ * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer (2's complement
+ * notation).
+ */
+ static final short TYPE_LONG = 9;
+ /**
+ * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first one is the numerator
+ * and the second one is the denominator.
+ */
+ static final short TYPE_RATIONAL = 10;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+ private static final int[] TYPE_TO_SIZE_MAP = new int[11];
+ private static final int UNSIGNED_SHORT_MAX = 65535;
+ private static final long UNSIGNED_LONG_MAX = 4294967295L;
+ private static final long LONG_MAX = Integer.MAX_VALUE;
+ private static final long LONG_MIN = Integer.MIN_VALUE;
+
+ static {
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8;
+ TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+ }
+
+ static final int SIZE_UNDEFINED = 0;
+
+ // Exif TagId
+ private final short mTagId;
+ // Exif Tag Type
+ private final short mDataType;
+ // If tag has defined count
+ private boolean mHasDefinedDefaultComponentCount;
+ // Actual data count in tag (should be number of elements in value array)
+ private int mComponentCountActual;
+ // The ifd that this tag should be put in
+ private int mIfd;
+ // The value (array of elements of type Tag Type)
+ private Object mValue;
+ // Value offset in exif header.
+ private int mOffset;
+
+ /** Returns true if the given IFD is a valid IFD. */
+ static boolean isValidIfd(int ifdId) {
+ return ifdId == IfdId.TYPE_IFD_0
+ || ifdId == IfdId.TYPE_IFD_1
+ || ifdId == IfdId.TYPE_IFD_EXIF
+ || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+ || ifdId == IfdId.TYPE_IFD_GPS;
+ }
+
+ /** Returns true if a given type is a valid tag type. */
+ static boolean isValidType(short type) {
+ return type == TYPE_UNSIGNED_BYTE
+ || type == TYPE_ASCII
+ || type == TYPE_UNSIGNED_SHORT
+ || type == TYPE_UNSIGNED_LONG
+ || type == TYPE_UNSIGNED_RATIONAL
+ || type == TYPE_UNDEFINED
+ || type == TYPE_LONG
+ || type == TYPE_RATIONAL;
+ }
+
+ // Use builtTag in ExifInterface instead of constructor.
+ ExifTag(short tagId, short type, int componentCount, int ifd, boolean hasDefinedComponentCount) {
+ mTagId = tagId;
+ mDataType = type;
+ mComponentCountActual = componentCount;
+ mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
+ mIfd = ifd;
+ mValue = null;
+ }
+
+ /**
+ * Gets the element size of the given data type in bytes.
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ private static int getElementSize(short type) {
+ return TYPE_TO_SIZE_MAP[type];
+ }
+
+ /**
+ * Returns the ID of the IFD this tag belongs to.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ int getIfd() {
+ return mIfd;
+ }
+
+ void setIfd(int ifdId) {
+ mIfd = ifdId;
+ }
+
+ /** Gets the TID of this tag. */
+ short getTagId() {
+ return mTagId;
+ }
+
+ /**
+ * Gets the data type of this tag
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ short getDataType() {
+ return mDataType;
+ }
+
+ /** Gets the total data size in bytes of the value of this tag. */
+ int getDataSize() {
+ return getComponentCount() * getElementSize(getDataType());
+ }
+
+ /** Gets the component count of this tag. */
+
+ // TODO: fix integer overflows with this
+ int getComponentCount() {
+ return mComponentCountActual;
+ }
+
+ /**
+ * Sets the component count of this tag. Call this function before setValue() if the length of
+ * value does not match the component count.
+ */
+ void forceSetComponentCount(int count) {
+ mComponentCountActual = count;
+ }
+
+ /**
+ * Returns true if this ExifTag contains value; otherwise, this tag will contain an offset value
+ * that is determined when the tag is written.
+ */
+ boolean hasValue() {
+ return mValue != null;
+ }
+
+ /**
+ * Sets integer values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, {@link
+ * #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(int[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_SHORT
+ && mDataType != TYPE_LONG
+ && mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+ return false;
+ } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+
+ long[] data = new long[value.length];
+ for (int i = 0; i < value.length; i++) {
+ data[i] = value[i];
+ }
+ mValue = data;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets long values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_LONG}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(long[] value) {
+ if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets a string value into this tag. This method should be used for tags of type {@link
+ * #TYPE_ASCII}. The string is converted to an ASCII string. Characters that cannot be converted
+ * are replaced with '?'. The length of the string must be equal to either (component count -1) or
+ * (component count). The final byte will be set to the string null terminator '\0', overwriting
+ * the last character in the string if the value.length is equal to the component count. This
+ * method will fail if:
+ *
+ * <ul>
+ * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.
+ * <li>The length of the string is not equal to (component count -1) or (component count) in the
+ * definition for this tag.
+ * </ul>
+ */
+ boolean setValue(String value) {
+ if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+
+ byte[] buf = value.getBytes(US_ASCII);
+ byte[] finalBuf = buf;
+ if (buf.length > 0) {
+ finalBuf =
+ (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED)
+ ? buf
+ : Arrays.copyOf(buf, buf.length + 1);
+ } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) {
+ finalBuf = new byte[] {0};
+ }
+ int count = finalBuf.length;
+ if (checkBadComponentCount(count)) {
+ return false;
+ }
+ mComponentCountActual = count;
+ mValue = finalBuf;
+ return true;
+ }
+
+ /**
+ * Sets Rational values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or {@link
+ * #TYPE_RATIONAL}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ *
+ * @see Rational
+ */
+ boolean setValue(Rational[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+ return false;
+ } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+ return false;
+ }
+
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets byte values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or {@link
+ * #TYPE_UNDEFINED} .
+ * <li>The length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ private boolean setValue(byte[] value, int offset, int length) {
+ if (checkBadComponentCount(length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+ mValue = new byte[length];
+ System.arraycopy(value, offset, mValue, 0, length);
+ mComponentCountActual = length;
+ return true;
+ }
+
+ /** Equivalent to setValue(value, 0, value.length). */
+ boolean setValue(byte[] value) {
+ return setValue(value, 0, value.length);
+ }
+
+ /**
+ * Gets the value as an array of ints. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @return the value as as an array of ints, or null if the tag's value does not exist or cannot
+ * be converted to an array of ints.
+ */
+ int[] getValueAsInts() {
+ if (mValue == null) {
+ return null;
+ } else if (mValue instanceof long[]) {
+ long[] val = (long[]) mValue;
+ int[] arr = new int[val.length];
+ for (int i = 0; i < val.length; i++) {
+ arr[i] = (int) val[i]; // Truncates
+ }
+ return arr;
+ }
+ return null;
+ }
+
+ /** Gets the tag's value or null if none exists. */
+ public Object getValue() {
+ return mValue;
+ }
+
+ /** Gets a string representation of the value. */
+ private String forceGetValueAsString() {
+ if (mValue == null) {
+ return "";
+ } else if (mValue instanceof byte[]) {
+ if (mDataType == TYPE_ASCII) {
+ return new String((byte[]) mValue, US_ASCII);
+ } else {
+ return Arrays.toString((byte[]) mValue);
+ }
+ } else if (mValue instanceof long[]) {
+ if (((long[]) mValue).length == 1) {
+ return String.valueOf(((long[]) mValue)[0]);
+ } else {
+ return Arrays.toString((long[]) mValue);
+ }
+ } else if (mValue instanceof Object[]) {
+ if (((Object[]) mValue).length == 1) {
+ Object val = ((Object[]) mValue)[0];
+ if (val == null) {
+ return "";
+ } else {
+ return val.toString();
+ }
+ } else {
+ return Arrays.toString((Object[]) mValue);
+ }
+ } else {
+ return mValue.toString();
+ }
+ }
+
+ /**
+ * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG}, {@link #TYPE_UNDEFINED},
+ * {@link #TYPE_UNSIGNED_BYTE}, {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}.
+ *
+ * @exception IllegalArgumentException if the data type is {@link #TYPE_RATIONAL} or {@link
+ * #TYPE_UNSIGNED_RATIONAL}.
+ */
+ long getValueAt(int index) {
+ if (mValue instanceof long[]) {
+ return ((long[]) mValue)[index];
+ } else if (mValue instanceof byte[]) {
+ return ((byte[]) mValue)[index];
+ }
+ throw new IllegalArgumentException(
+ "Cannot get integer value from " + convertTypeToString(mDataType));
+ }
+
+ /**
+ * Gets the {@link #TYPE_ASCII} data.
+ *
+ * @exception IllegalArgumentException If the type is NOT {@link #TYPE_ASCII}.
+ */
+ protected String getString() {
+ if (mDataType != TYPE_ASCII) {
+ throw new IllegalArgumentException(
+ "Cannot get ASCII value from " + convertTypeToString(mDataType));
+ }
+ return new String((byte[]) mValue, US_ASCII);
+ }
+
+ /**
+ * Gets the offset of this tag. This is only valid if this data size > 4 and contains an offset to
+ * the location of the actual value.
+ */
+ protected int getOffset() {
+ return mOffset;
+ }
+
+ /** Sets the offset of this tag. */
+ protected void setOffset(int offset) {
+ mOffset = offset;
+ }
+
+ void setHasDefinedCount(boolean d) {
+ mHasDefinedDefaultComponentCount = d;
+ }
+
+ boolean hasDefinedCount() {
+ return mHasDefinedDefaultComponentCount;
+ }
+
+ private boolean checkBadComponentCount(int count) {
+ return mHasDefinedDefaultComponentCount && (mComponentCountActual != count);
+ }
+
+ private static String convertTypeToString(short type) {
+ switch (type) {
+ case TYPE_UNSIGNED_BYTE:
+ return "UNSIGNED_BYTE";
+ case TYPE_ASCII:
+ return "ASCII";
+ case TYPE_UNSIGNED_SHORT:
+ return "UNSIGNED_SHORT";
+ case TYPE_UNSIGNED_LONG:
+ return "UNSIGNED_LONG";
+ case TYPE_UNSIGNED_RATIONAL:
+ return "UNSIGNED_RATIONAL";
+ case TYPE_UNDEFINED:
+ return "UNDEFINED";
+ case TYPE_LONG:
+ return "LONG";
+ case TYPE_RATIONAL:
+ return "RATIONAL";
+ default:
+ return "";
+ }
+ }
+
+ private boolean checkOverflowForUnsignedShort(int[] value) {
+ for (int v : value) {
+ if (v > UNSIGNED_SHORT_MAX || v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(long[] value) {
+ for (long v : value) {
+ if (v < 0 || v > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(int[] value) {
+ for (int v : value) {
+ if (v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < 0
+ || v.getDenominator() < 0
+ || v.getNumerator() > UNSIGNED_LONG_MAX
+ || v.getDenominator() > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < LONG_MIN
+ || v.getDenominator() < LONG_MIN
+ || v.getNumerator() > LONG_MAX
+ || v.getDenominator() > LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof ExifTag) {
+ ExifTag tag = (ExifTag) obj;
+ if (tag.mTagId != this.mTagId
+ || tag.mComponentCountActual != this.mComponentCountActual
+ || tag.mDataType != this.mDataType) {
+ return false;
+ }
+ if (mValue != null) {
+ if (tag.mValue == null) {
+ return false;
+ } else if (mValue instanceof long[]) {
+ if (!(tag.mValue instanceof long[])) {
+ return false;
+ }
+ return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
+ } else if (mValue instanceof Rational[]) {
+ if (!(tag.mValue instanceof Rational[])) {
+ return false;
+ }
+ return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
+ } else if (mValue instanceof byte[]) {
+ if (!(tag.mValue instanceof byte[])) {
+ return false;
+ }
+ return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
+ } else {
+ return mValue.equals(tag.mValue);
+ }
+ } else {
+ return tag.mValue == null;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTagId,
+ mDataType,
+ mHasDefinedDefaultComponentCount,
+ mComponentCountActual,
+ mIfd,
+ mValue,
+ mOffset);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tag id: %04X\n", mTagId)
+ + "ifd id: "
+ + mIfd
+ + "\ntype: "
+ + convertTypeToString(mDataType)
+ + "\ncount: "
+ + mComponentCountActual
+ + "\noffset: "
+ + mOffset
+ + "\nvalue: "
+ + forceGetValueAsString()
+ + "\n";
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdData.java b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
new file mode 100644
index 000000000..b808defc6
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class stores all the tags in an IFD.
+ *
+ * @see ExifData
+ * @see ExifTag
+ */
+class IfdData {
+
+ private final int mIfdId;
+ private final Map<Short, ExifTag> mExifTags = new HashMap<>();
+ private static final int[] sIfds = {
+ IfdId.TYPE_IFD_0,
+ IfdId.TYPE_IFD_1,
+ IfdId.TYPE_IFD_EXIF,
+ IfdId.TYPE_IFD_INTEROPERABILITY,
+ IfdId.TYPE_IFD_GPS
+ };
+ /**
+ * Creates an IfdData with given IFD ID.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ IfdData(int ifdId) {
+ mIfdId = ifdId;
+ }
+
+ static int[] getIfds() {
+ return sIfds;
+ }
+
+ /** Get a array the contains all {@link ExifTag} in this IFD. */
+ private ExifTag[] getAllTags() {
+ return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
+ }
+
+ /**
+ * Gets the ID of this IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ protected int getId() {
+ return mIfdId;
+ }
+
+ /** Gets the {@link ExifTag} with given tag id. Return null if there is no such tag. */
+ protected ExifTag getTag(short tagId) {
+ return mExifTags.get(tagId);
+ }
+
+ /** Adds or replaces a {@link ExifTag}. */
+ protected ExifTag setTag(ExifTag tag) {
+ tag.setIfd(mIfdId);
+ return mExifTags.put(tag.getTagId(), tag);
+ }
+
+ /** Gets the tags count in the IFD. */
+ private int getTagCount() {
+ return mExifTags.size();
+ }
+
+ /**
+ * Returns true if all tags in this two IFDs are equal. Note that tags of IFDs offset or thumbnail
+ * offset will be ignored.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof IfdData) {
+ IfdData data = (IfdData) obj;
+ if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
+ ExifTag[] tags = data.getAllTags();
+ for (ExifTag tag : tags) {
+ if (ExifInterface.isOffsetTag(tag.getTagId())) {
+ continue;
+ }
+ ExifTag tag2 = mExifTags.get(tag.getTagId());
+ if (!tag.equals(tag2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIfdId, mExifTags);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdId.java b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
new file mode 100644
index 000000000..c61545752
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+/** The constants of the IFD ID defined in EXIF spec. */
+public interface IfdId {
+ int TYPE_IFD_0 = 0;
+ int TYPE_IFD_1 = 1;
+ int TYPE_IFD_EXIF = 2;
+ int TYPE_IFD_INTEROPERABILITY = 3;
+ int TYPE_IFD_GPS = 4;
+ /* This is used in ExifData to allocate enough IfdData */
+ int TYPE_IFD_COUNT = 5;
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
new file mode 100644
index 000000000..3d98fcc0e
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+class JpegHeader {
+ static final short SOI = (short) 0xFFD8;
+ static final short APP1 = (short) 0xFFE1;
+ static final short EOI = (short) 0xFFD9;
+
+ /**
+ * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG, and
+ * DAC marker.
+ */
+ private static final short SOF0 = (short) 0xFFC0;
+
+ private static final short SOF15 = (short) 0xFFCF;
+ private static final short DHT = (short) 0xFFC4;
+ private static final short JPG = (short) 0xFFC8;
+ private static final short DAC = (short) 0xFFCC;
+
+ static boolean isSofMarker(short marker) {
+ return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG && marker != DAC;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/Rational.java b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
new file mode 100644
index 000000000..9afca8449
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callcomposer.camera.exif;
+
+import java.util.Objects;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the numerator and
+ * denominator of a Rational number.
+ */
+public class Rational {
+
+ private final long mNumerator;
+ private final long mDenominator;
+
+ /** Create a Rational with a given numerator and denominator. */
+ Rational(long nominator, long denominator) {
+ mNumerator = nominator;
+ mDenominator = denominator;
+ }
+
+ /** Gets the numerator of the rational. */
+ long getNumerator() {
+ return mNumerator;
+ }
+
+ /** Gets the denominator of the rational */
+ long getDenominator() {
+ return mDenominator;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Rational) {
+ Rational data = (Rational) obj;
+ return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNumerator, mDenominator);
+ }
+
+ @Override
+ public String toString() {
+ return mNumerator + "/" + mDenominator;
+ }
+}