From ed15d1a140986473bbe7fffd72ec9618c41c5979 Mon Sep 17 00:00:00 2001 From: Angus Kong Date: Mon, 19 Aug 2013 15:06:12 -0700 Subject: Bring back wide angle panorama. bug:10293937 Change-Id: I23a977e87b7416f07ecac20025b6c142ae61be05 --- Android.mk | 9 + res/drawable-hdpi/ic_switch_pan.png | Bin 0 -> 5172 bytes res/drawable-mdpi/ic_switch_pan.png | Bin 0 -> 3063 bytes res/drawable-xhdpi/ic_switch_pan.png | Bin 0 -> 7660 bytes res/drawable/pano_direction_left_indicator.xml | 21 + res/drawable/pano_direction_right_indicator.xml | 21 + res/layout-land/camera_controls.xml | 2 +- res/layout-land/pano_module_capture.xml | 96 ++ res/layout-land/pano_module_review.xml | 48 + res/layout-port/camera_controls.xml | 4 +- res/layout-port/pano_module_capture.xml | 96 ++ res/layout-port/pano_module_review.xml | 60 ++ res/layout/panorama_module.xml | 26 + res/values-land/styles.xml | 7 - res/values-port/styles.xml | 7 - res/values/styles.xml | 7 + src/com/android/camera/CameraActivity.java | 46 +- src/com/android/camera/Mosaic.java | 206 ++++ src/com/android/camera/MosaicFrameProcessor.java | 237 +++++ src/com/android/camera/MosaicPreviewRenderer.java | 190 ++++ src/com/android/camera/MosaicRenderer.java | 89 ++ src/com/android/camera/PanoProgressBar.java | 188 ++++ src/com/android/camera/PanoUtil.java | 86 ++ src/com/android/camera/PhotoUI.java | 12 +- src/com/android/camera/PreviewFrameLayout.java | 137 --- src/com/android/camera/VideoUI.java | 12 +- .../camera/WideAnglePanoramaController.java | 33 + .../android/camera/WideAnglePanoramaModule.java | 1071 ++++++++++++++++++++ src/com/android/camera/WideAnglePanoramaUI.java | 461 +++++++++ src/com/android/camera/app/AppManagerFactory.java | 47 + src/com/android/camera/app/CameraApp.java | 1 + src/com/android/camera/app/OrientationManager.java | 25 +- src/com/android/camera/app/OrientationSource.java | 6 - src/com/android/camera/ui/CameraRootView.java | 4 +- src/com/android/camera/ui/CameraSwitcher.java | 386 ------- src/com/android/camera/ui/LayoutChangeHelper.java | 43 - .../android/camera/ui/LayoutChangeNotifier.java | 28 - src/com/android/camera/ui/LayoutNotifyView.java | 48 - src/com/android/camera/ui/ModuleSwitcher.java | 392 +++++++ .../android/camera/PanoramaStitchingManager.java | 25 - .../camera/app/PanoramaStitchingManager.java | 43 + 41 files changed, 3488 insertions(+), 732 deletions(-) create mode 100644 res/drawable-hdpi/ic_switch_pan.png create mode 100644 res/drawable-mdpi/ic_switch_pan.png create mode 100644 res/drawable-xhdpi/ic_switch_pan.png create mode 100644 res/drawable/pano_direction_left_indicator.xml create mode 100644 res/drawable/pano_direction_right_indicator.xml create mode 100644 res/layout-land/pano_module_capture.xml create mode 100644 res/layout-land/pano_module_review.xml create mode 100644 res/layout-port/pano_module_capture.xml create mode 100644 res/layout-port/pano_module_review.xml create mode 100644 res/layout/panorama_module.xml create mode 100644 src/com/android/camera/Mosaic.java create mode 100644 src/com/android/camera/MosaicFrameProcessor.java create mode 100644 src/com/android/camera/MosaicPreviewRenderer.java create mode 100644 src/com/android/camera/MosaicRenderer.java create mode 100644 src/com/android/camera/PanoProgressBar.java create mode 100644 src/com/android/camera/PanoUtil.java delete mode 100644 src/com/android/camera/PreviewFrameLayout.java create mode 100644 src/com/android/camera/WideAnglePanoramaController.java create mode 100644 src/com/android/camera/WideAnglePanoramaModule.java create mode 100644 src/com/android/camera/WideAnglePanoramaUI.java create mode 100644 src/com/android/camera/app/AppManagerFactory.java delete mode 100644 src/com/android/camera/app/OrientationSource.java delete mode 100644 src/com/android/camera/ui/CameraSwitcher.java delete mode 100644 src/com/android/camera/ui/LayoutChangeHelper.java delete mode 100644 src/com/android/camera/ui/LayoutChangeNotifier.java delete mode 100644 src/com/android/camera/ui/LayoutNotifyView.java create mode 100644 src/com/android/camera/ui/ModuleSwitcher.java delete mode 100644 src_pd/com/android/camera/PanoramaStitchingManager.java create mode 100644 src_pd/com/android/camera/app/PanoramaStitchingManager.java diff --git a/Android.mk b/Android.mk index a3659d095..ef4772844 100644 --- a/Android.mk +++ b/Android.mk @@ -26,6 +26,15 @@ LOCAL_SDK_VERSION := current LOCAL_PROGUARD_FLAG_FILES := proguard.flags +# If this is an unbundled build (to install seprately) then include +# the libraries in the APK, otherwise just put them in /system/lib and +# leave them out of the APK +ifneq (,$(TARGET_BUILD_APPS)) + LOCAL_JNI_SHARED_LIBRARIES := libjni_mosaic +else + LOCAL_REQUIRED_MODULES := libjni_mosaic +endif + include $(BUILD_PACKAGE) include $(call all-makefiles-under, $(LOCAL_PATH)) diff --git a/res/drawable-hdpi/ic_switch_pan.png b/res/drawable-hdpi/ic_switch_pan.png new file mode 100644 index 000000000..c8161be3a Binary files /dev/null and b/res/drawable-hdpi/ic_switch_pan.png differ diff --git a/res/drawable-mdpi/ic_switch_pan.png b/res/drawable-mdpi/ic_switch_pan.png new file mode 100644 index 000000000..e63b8e968 Binary files /dev/null and b/res/drawable-mdpi/ic_switch_pan.png differ diff --git a/res/drawable-xhdpi/ic_switch_pan.png b/res/drawable-xhdpi/ic_switch_pan.png new file mode 100644 index 000000000..f17ce2f4a Binary files /dev/null and b/res/drawable-xhdpi/ic_switch_pan.png differ diff --git a/res/drawable/pano_direction_left_indicator.xml b/res/drawable/pano_direction_left_indicator.xml new file mode 100644 index 000000000..a0bfb0af3 --- /dev/null +++ b/res/drawable/pano_direction_left_indicator.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/res/drawable/pano_direction_right_indicator.xml b/res/drawable/pano_direction_right_indicator.xml new file mode 100644 index 000000000..c3ce37797 --- /dev/null +++ b/res/drawable/pano_direction_right_indicator.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/res/layout-land/camera_controls.xml b/res/layout-land/camera_controls.xml index d1772401e..14953320e 100644 --- a/res/layout-land/camera_controls.xml +++ b/res/layout-land/camera_controls.xml @@ -40,7 +40,7 @@ android:layout_gravity="right|top" android:layout_marginRight="2dip" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout-land/pano_module_review.xml b/res/layout-land/pano_module_review.xml new file mode 100644 index 000000000..002d47aff --- /dev/null +++ b/res/layout-land/pano_module_review.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/res/layout-port/camera_controls.xml b/res/layout-port/camera_controls.xml index 5f89830c5..03e896bc0 100644 --- a/res/layout-port/camera_controls.xml +++ b/res/layout-port/camera_controls.xml @@ -40,7 +40,7 @@ android:layout_marginBottom="2dip" android:contentDescription="@string/accessibility_menu_button" /> - - \ No newline at end of file + diff --git a/res/layout-port/pano_module_capture.xml b/res/layout-port/pano_module_capture.xml new file mode 100644 index 000000000..57c00cded --- /dev/null +++ b/res/layout-port/pano_module_capture.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout-port/pano_module_review.xml b/res/layout-port/pano_module_review.xml new file mode 100644 index 000000000..3c5eb2cfc --- /dev/null +++ b/res/layout-port/pano_module_review.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/panorama_module.xml b/res/layout/panorama_module.xml new file mode 100644 index 000000000..64063a20d --- /dev/null +++ b/res/layout/panorama_module.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml index 6ca7e9175..69b524e70 100644 --- a/res/values-land/styles.xml +++ b/res/values-land/styles.xml @@ -55,13 +55,6 @@ @dimen/indicator_bar_width 13dp 13dp - - - + diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java index 47a964f06..ec8fc956b 100644 --- a/src/com/android/camera/CameraActivity.java +++ b/src/com/android/camera/CameraActivity.java @@ -53,6 +53,8 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.ShareActionProvider; +import com.android.camera.app.AppManagerFactory; +import com.android.camera.app.PanoramaStitchingManager; import com.android.camera.data.CameraDataAdapter; import com.android.camera.data.CameraPreviewData; import com.android.camera.data.FixedFirstDataAdapter; @@ -61,8 +63,7 @@ import com.android.camera.data.LocalData; import com.android.camera.data.LocalDataAdapter; import com.android.camera.data.MediaDetails; import com.android.camera.data.SimpleViewData; -import com.android.camera.ui.CameraSwitcher; -import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.ModuleSwitcher; import com.android.camera.ui.DetailsDialog; import com.android.camera.ui.FilmStripView; import com.android.camera.util.ApiHelper; @@ -72,7 +73,7 @@ import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera2.R; public class CameraActivity extends Activity - implements CameraSwitchListener { + implements ModuleSwitcher.ModuleSwitchListener { private static final String TAG = "CAM_Activity"; @@ -128,7 +129,6 @@ public class CameraActivity extends Activity private boolean mSecureCamera; // This is a hack to speed up the start of SecureCamera. private static boolean sFirstStartAfterScreenOn = true; - private boolean mShowCameraPreview; private int mLastRawOrientation; private MyOrientationEventListener mOrientationListener; private Handler mMainHandler; @@ -643,7 +643,8 @@ public class CameraActivity extends Activity mAboveFilmstripControlLayout.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - mPanoramaManager = new PanoramaStitchingManager(CameraActivity.this); + mPanoramaManager = AppManagerFactory.getInstance(this) + .getPanoramaStitchingManager(); mPanoramaManager.addTaskListener(mStitchingListener); LayoutInflater inflater = getLayoutInflater(); View rootLayout = inflater.inflate(R.layout.camera, null, false); @@ -670,26 +671,26 @@ public class CameraActivity extends Activity int moduleIndex = -1; if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(getIntent().getAction()) || MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())) { - moduleIndex = CameraSwitcher.VIDEO_MODULE_INDEX; + moduleIndex = ModuleSwitcher.VIDEO_MODULE_INDEX; } else if (MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(getIntent().getAction()) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(getIntent() .getAction()) || MediaStore.ACTION_IMAGE_CAPTURE.equals(getIntent().getAction()) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(getIntent().getAction())) { - moduleIndex = CameraSwitcher.PHOTO_MODULE_INDEX; + moduleIndex = ModuleSwitcher.PHOTO_MODULE_INDEX; } else { // If the activity has not been started using an explicit intent, // read the module index from the last time the user changed modes SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); moduleIndex = prefs.getInt(PREF_STARTUP_MODULE_INDEX, -1); if (moduleIndex < 0) { - moduleIndex = CameraSwitcher.PHOTO_MODULE_INDEX; + moduleIndex = ModuleSwitcher.PHOTO_MODULE_INDEX; } } - setModuleFromIndex(moduleIndex); - mCurrentModule.init(this, mCameraModuleRootView); mOrientationListener = new MyOrientationEventListener(this); + setModuleFromIndex(moduleIndex); + mCurrentModule.init(this, mCameraModuleRootView); mMainHandler = new Handler(getMainLooper()); if (!mSecureCamera) { @@ -788,9 +789,6 @@ public class CameraActivity extends Activity || keyCode == KeyEvent.KEYCODE_MENU) { if (event.isLongPress()) return true; } - if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) { - return true; - } return super.onKeyDown(keyCode, event); } @@ -798,9 +796,6 @@ public class CameraActivity extends Activity @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (mCurrentModule.onKeyUp(keyCode, event)) return true; - if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) { - return true; - } return super.onKeyUp(keyCode, event); } @@ -887,7 +882,7 @@ public class CameraActivity extends Activity } @Override - public void onCameraSelected(int moduleIndex) { + public void onModuleSelected(int moduleIndex) { if (mCurrentModuleIndex == moduleIndex) return; CameraHolder.instance().keep(); @@ -913,15 +908,26 @@ public class CameraActivity extends Activity private void setModuleFromIndex(int moduleIndex) { mCurrentModuleIndex = moduleIndex; switch (moduleIndex) { - case CameraSwitcher.VIDEO_MODULE_INDEX: + case ModuleSwitcher.VIDEO_MODULE_INDEX: { mCurrentModule = new VideoModule(); break; - case CameraSwitcher.PHOTO_MODULE_INDEX: + } + + case ModuleSwitcher.PHOTO_MODULE_INDEX: { mCurrentModule = new PhotoModule(); break; - case CameraSwitcher.LIGHTCYCLE_MODULE_INDEX: + } + + case ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX: { + mCurrentModule = new WideAnglePanoramaModule(); + break; + } + + case ModuleSwitcher.LIGHTCYCLE_MODULE_INDEX: { mCurrentModule = PhotoSphereHelper.createPanoramaModule(); break; + } + default: break; } diff --git a/src/com/android/camera/Mosaic.java b/src/com/android/camera/Mosaic.java new file mode 100644 index 000000000..b1d10c05b --- /dev/null +++ b/src/com/android/camera/Mosaic.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +/** + * The Java interface to JNI calls regarding mosaic stitching. + * + * A high-level usage is: + * + * Mosaic mosaic = new Mosaic(); + * mosaic.setSourceImageDimensions(width, height); + * mosaic.reset(blendType); + * + * while ((pixels = hasNextImage()) != null) { + * mosaic.setSourceImage(pixels); + * } + * + * mosaic.createMosaic(highRes); + * byte[] result = mosaic.getFinalMosaic(); + * + */ +public class Mosaic { + /** + * In this mode, the images are stitched together in the same spatial arrangement as acquired + * i.e. if the user follows a curvy trajectory, the image boundary of the resulting mosaic will + * be curved in the same manner. This mode is useful if the user wants to capture a mosaic as + * if "painting" the scene using the smart-phone device and does not want any corrective warps + * to distort the captured images. + */ + public static final int BLENDTYPE_FULL = 0; + + /** + * This mode is the same as BLENDTYPE_FULL except that the resulting mosaic is rotated + * to balance the first and last images to be approximately at the same vertical offset in the + * output mosaic. This is useful when acquiring a mosaic by a typical panning-like motion to + * remove a one-sided curve in the mosaic (typically due to the camera not staying horizontal + * during the video capture) and convert it to a more symmetrical "smiley-face" like output. + */ + public static final int BLENDTYPE_PAN = 1; + + /** + * This mode compensates for typical "smiley-face" like output in longer mosaics and creates + * a rectangular mosaic with minimal black borders (by unwrapping the mosaic onto an imaginary + * cylinder). If the user follows a curved trajectory (instead of a perfect panning trajectory), + * the resulting mosaic here may suffer from some image distortions in trying to map the + * trajectory to a cylinder. + */ + public static final int BLENDTYPE_CYLINDERPAN = 2; + + /** + * This mode is basically BLENDTYPE_CYLINDERPAN plus doing a rectangle cropping before returning + * the mosaic. The mode is useful for making the resulting mosaic have a rectangle shape. + */ + public static final int BLENDTYPE_HORIZONTAL =3; + + /** + * This strip type will use the default thin strips where the strips are + * spaced according to the image capture rate. + */ + public static final int STRIPTYPE_THIN = 0; + + /** + * This strip type will use wider strips for blending. The strip separation + * is controlled by a threshold on the native side. Since the strips are + * wider, there is an additional cross-fade blending step to make the seam + * boundaries smoother. Since this mode uses lesser image frames, it is + * computationally more efficient than the thin strip mode. + */ + public static final int STRIPTYPE_WIDE = 1; + + /** + * Return flags returned by createMosaic() are one of the following. + */ + public static final int MOSAIC_RET_OK = 1; + public static final int MOSAIC_RET_ERROR = -1; + public static final int MOSAIC_RET_CANCELLED = -2; + public static final int MOSAIC_RET_LOW_TEXTURE = -3; + public static final int MOSAIC_RET_FEW_INLIERS = 2; + + + static { + System.loadLibrary("jni_mosaic"); + } + + /** + * Allocate memory for the image frames at the given resolution. + * + * @param width width of the input frames in pixels + * @param height height of the input frames in pixels + */ + public native void allocateMosaicMemory(int width, int height); + + /** + * Free memory allocated by allocateMosaicMemory. + * + */ + public native void freeMosaicMemory(); + + /** + * Pass the input image frame to the native layer. Each time the a new + * source image t is set, the transformation matrix from the first source + * image to t is computed and returned. + * + * @param pixels source image of NV21 format. + * @return Float array of length 11; first 9 entries correspond to the 3x3 + * transformation matrix between the first frame and the passed frame; + * the 10th entry is the number of the passed frame, where the counting + * starts from 1; and the 11th entry is the returning code, whose value + * is one of those MOSAIC_RET_* returning flags defined above. + */ + public native float[] setSourceImage(byte[] pixels); + + /** + * This is an alternative to the setSourceImage function above. This should + * be called when the image data is already on the native side in a fixed + * byte array. In implementation, this array is filled by the GL thread + * using glReadPixels directly from GPU memory (where it is accessed by + * an associated SurfaceTexture). + * + * @return Float array of length 11; first 9 entries correspond to the 3x3 + * transformation matrix between the first frame and the passed frame; + * the 10th entry is the number of the passed frame, where the counting + * starts from 1; and the 11th entry is the returning code, whose value + * is one of those MOSAIC_RET_* returning flags defined above. + */ + public native float[] setSourceImageFromGPU(); + + /** + * Set the type of blending. + * + * @param type the blending type defined in the class. {BLENDTYPE_FULL, + * BLENDTYPE_PAN, BLENDTYPE_CYLINDERPAN, BLENDTYPE_HORIZONTAL} + */ + public native void setBlendingType(int type); + + /** + * Set the type of strips to use for blending. + * @param type the blending strip type to use {STRIPTYPE_THIN, + * STRIPTYPE_WIDE}. + */ + public native void setStripType(int type); + + /** + * Tell the native layer to create the final mosaic after all the input frame + * data have been collected. + * The case of generating high-resolution mosaic may take dozens of seconds to finish. + * + * @param value True means generating a high-resolution mosaic - + * which is based on the original images set in setSourceImage(). + * False means generating a low-resolution version - + * which is based on 1/4 downscaled images from the original images. + * @return Returns a status code suggesting if the mosaic building was + * successful, in error, or was cancelled by the user. + */ + public native int createMosaic(boolean value); + + /** + * Get the data for the created mosaic. + * + * @return Returns an integer array which contains the final mosaic in the ARGB_8888 format. + * The first MosaicWidth*MosaicHeight values contain the image data, followed by 2 + * integers corresponding to the values MosaicWidth and MosaicHeight respectively. + */ + public native int[] getFinalMosaic(); + + /** + * Get the data for the created mosaic. + * + * @return Returns a byte array which contains the final mosaic in the NV21 format. + * The first MosaicWidth*MosaicHeight*1.5 values contain the image data, followed by + * 8 bytes which pack the MosaicWidth and MosaicHeight integers into 4 bytes each + * respectively. + */ + public native byte[] getFinalMosaicNV21(); + + /** + * Reset the state of the frame arrays which maintain the captured frame data. + * Also re-initializes the native mosaic object to make it ready for capturing a new mosaic. + */ + public native void reset(); + + /** + * Get the progress status of the mosaic computation process. + * @param hires Boolean flag to select whether to report progress of the + * low-res or high-res mosaicer. + * @param cancelComputation Boolean flag to allow cancelling the + * mosaic computation when needed from the GUI end. + * @return Returns a number from 0-100 where 50 denotes that the mosaic + * computation is 50% done. + */ + public native int reportProgress(boolean hires, boolean cancelComputation); +} diff --git a/src/com/android/camera/MosaicFrameProcessor.java b/src/com/android/camera/MosaicFrameProcessor.java new file mode 100644 index 000000000..cb305344d --- /dev/null +++ b/src/com/android/camera/MosaicFrameProcessor.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2011 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.camera; + +import android.util.Log; + +/** + * A singleton to handle the processing of each frame by {@link Mosaic}. + */ +public class MosaicFrameProcessor { + private static final String TAG = "MosaicFrameProcessor"; + private static final int NUM_FRAMES_IN_BUFFER = 2; + private static final int MAX_NUMBER_OF_FRAMES = 100; + private static final int MOSAIC_RET_CODE_INDEX = 10; + private static final int FRAME_COUNT_INDEX = 9; + private static final int X_COORD_INDEX = 2; + private static final int Y_COORD_INDEX = 5; + private static final int HR_TO_LR_DOWNSAMPLE_FACTOR = 4; + private static final int WINDOW_SIZE = 3; + + private Mosaic mMosaicer; + private boolean mIsMosaicMemoryAllocated = false; + private float mTranslationLastX; + private float mTranslationLastY; + + private int mFillIn = 0; + private int mTotalFrameCount = 0; + private int mLastProcessFrameIdx = -1; + private int mCurrProcessFrameIdx = -1; + private boolean mFirstRun; + + // Panning rate is in unit of percentage of image content translation per + // frame. Use moving average to calculate the panning rate. + private float mPanningRateX; + private float mPanningRateY; + + private float[] mDeltaX = new float[WINDOW_SIZE]; + private float[] mDeltaY = new float[WINDOW_SIZE]; + private int mOldestIdx = 0; + private float mTotalTranslationX = 0f; + private float mTotalTranslationY = 0f; + + private ProgressListener mProgressListener; + + private int mPreviewWidth; + private int mPreviewHeight; + private int mPreviewBufferSize; + + private static MosaicFrameProcessor sMosaicFrameProcessor; // singleton + + public interface ProgressListener { + public void onProgress(boolean isFinished, float panningRateX, float panningRateY, + float progressX, float progressY); + } + + public static MosaicFrameProcessor getInstance() { + if (sMosaicFrameProcessor == null) { + sMosaicFrameProcessor = new MosaicFrameProcessor(); + } + return sMosaicFrameProcessor; + } + + private MosaicFrameProcessor() { + mMosaicer = new Mosaic(); + } + + public void setProgressListener(ProgressListener listener) { + mProgressListener = listener; + } + + public int reportProgress(boolean hires, boolean cancel) { + return mMosaicer.reportProgress(hires, cancel); + } + + public void initialize(int previewWidth, int previewHeight, int bufSize) { + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + mPreviewBufferSize = bufSize; + setupMosaicer(mPreviewWidth, mPreviewHeight, mPreviewBufferSize); + setStripType(Mosaic.STRIPTYPE_WIDE); + // no need to call reset() here. reset() should be called by the client + // after this initialization before calling other methods of this object. + } + + public void clear() { + if (mIsMosaicMemoryAllocated) { + mMosaicer.freeMosaicMemory(); + mIsMosaicMemoryAllocated = false; + } + synchronized (this) { + notify(); + } + } + + public boolean isMosaicMemoryAllocated() { + return mIsMosaicMemoryAllocated; + } + + public void setStripType(int type) { + mMosaicer.setStripType(type); + } + + private void setupMosaicer(int previewWidth, int previewHeight, int bufSize) { + Log.v(TAG, "setupMosaicer w, h=" + previewWidth + ',' + previewHeight + ',' + bufSize); + + if (mIsMosaicMemoryAllocated) throw new RuntimeException("MosaicFrameProcessor in use!"); + mIsMosaicMemoryAllocated = true; + mMosaicer.allocateMosaicMemory(previewWidth, previewHeight); + } + + public void reset() { + // reset() can be called even if MosaicFrameProcessor is not initialized. + // Only counters will be changed. + mFirstRun = true; + mTotalFrameCount = 0; + mFillIn = 0; + mTotalTranslationX = 0; + mTranslationLastX = 0; + mTotalTranslationY = 0; + mTranslationLastY = 0; + mPanningRateX = 0; + mPanningRateY = 0; + mLastProcessFrameIdx = -1; + mCurrProcessFrameIdx = -1; + for (int i = 0; i < WINDOW_SIZE; ++i) { + mDeltaX[i] = 0f; + mDeltaY[i] = 0f; + } + mMosaicer.reset(); + } + + public int createMosaic(boolean highRes) { + return mMosaicer.createMosaic(highRes); + } + + public byte[] getFinalMosaicNV21() { + return mMosaicer.getFinalMosaicNV21(); + } + + // Processes the last filled image frame through the mosaicer and + // updates the UI to show progress. + // When done, processes and displays the final mosaic. + public void processFrame() { + if (!mIsMosaicMemoryAllocated) { + // clear() is called and buffers are cleared, stop computation. + // This can happen when the onPause() is called in the activity, but still some frames + // are not processed yet and thus the callback may be invoked. + return; + } + + mCurrProcessFrameIdx = mFillIn; + mFillIn = ((mFillIn + 1) % NUM_FRAMES_IN_BUFFER); + + // Check that we are trying to process a frame different from the + // last one processed (useful if this class was running asynchronously) + if (mCurrProcessFrameIdx != mLastProcessFrameIdx) { + mLastProcessFrameIdx = mCurrProcessFrameIdx; + + // TODO: make the termination condition regarding reaching + // MAX_NUMBER_OF_FRAMES solely determined in the library. + if (mTotalFrameCount < MAX_NUMBER_OF_FRAMES) { + // If we are still collecting new frames for the current mosaic, + // process the new frame. + calculateTranslationRate(); + + // Publish progress of the ongoing processing + if (mProgressListener != null) { + mProgressListener.onProgress(false, mPanningRateX, mPanningRateY, + mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth, + mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight); + } + } else { + if (mProgressListener != null) { + mProgressListener.onProgress(true, mPanningRateX, mPanningRateY, + mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth, + mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight); + } + } + } + } + + public void calculateTranslationRate() { + float[] frameData = mMosaicer.setSourceImageFromGPU(); + int ret_code = (int) frameData[MOSAIC_RET_CODE_INDEX]; + mTotalFrameCount = (int) frameData[FRAME_COUNT_INDEX]; + float translationCurrX = frameData[X_COORD_INDEX]; + float translationCurrY = frameData[Y_COORD_INDEX]; + + if (mFirstRun) { + // First time: no need to update delta values. + mTranslationLastX = translationCurrX; + mTranslationLastY = translationCurrY; + mFirstRun = false; + return; + } + + // Moving average: remove the oldest translation/deltaTime and + // add the newest translation/deltaTime in + int idx = mOldestIdx; + mTotalTranslationX -= mDeltaX[idx]; + mTotalTranslationY -= mDeltaY[idx]; + mDeltaX[idx] = Math.abs(translationCurrX - mTranslationLastX); + mDeltaY[idx] = Math.abs(translationCurrY - mTranslationLastY); + mTotalTranslationX += mDeltaX[idx]; + mTotalTranslationY += mDeltaY[idx]; + + // The panning rate is measured as the rate of the translation percentage in + // image width/height. Take the horizontal panning rate for example, the image width + // used in finding the translation is (PreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR). + // To get the horizontal translation percentage, the horizontal translation, + // (translationCurrX - mTranslationLastX), is divided by the + // image width. We then get the rate by dividing the translation percentage with the + // number of frames. + mPanningRateX = mTotalTranslationX / + (mPreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE; + mPanningRateY = mTotalTranslationY / + (mPreviewHeight / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE; + + mTranslationLastX = translationCurrX; + mTranslationLastY = translationCurrY; + mOldestIdx = (mOldestIdx + 1) % WINDOW_SIZE; + } +} diff --git a/src/com/android/camera/MosaicPreviewRenderer.java b/src/com/android/camera/MosaicPreviewRenderer.java new file mode 100644 index 000000000..e8c02db24 --- /dev/null +++ b/src/com/android/camera/MosaicPreviewRenderer.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import com.android.camera.util.ApiHelper; + +import javax.microedition.khronos.opengles.GL10; + +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class MosaicPreviewRenderer { + + @SuppressWarnings("unused") + private static final String TAG = "CAM_MosaicPreviewRenderer"; + + private int mWidth; // width of the view in UI + private int mHeight; // height of the view in UI + + private boolean mIsLandscape = true; + private final float[] mTransformMatrix = new float[16]; + + private ConditionVariable mEglThreadBlockVar = new ConditionVariable(); + private HandlerThread mEglThread; + private MyHandler mHandler; + private SurfaceTextureRenderer mSTRenderer; + + private SurfaceTexture mInputSurfaceTexture; + + private class MyHandler extends Handler { + public static final int MSG_INIT_SYNC = 0; + public static final int MSG_SHOW_PREVIEW_FRAME_SYNC = 1; + public static final int MSG_SHOW_PREVIEW_FRAME = 2; + public static final int MSG_ALIGN_FRAME_SYNC = 3; + public static final int MSG_RELEASE = 4; + + public MyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT_SYNC: + doInit(); + mEglThreadBlockVar.open(); + break; + case MSG_SHOW_PREVIEW_FRAME_SYNC: + doShowPreviewFrame(); + mEglThreadBlockVar.open(); + break; + case MSG_SHOW_PREVIEW_FRAME: + doShowPreviewFrame(); + break; + case MSG_ALIGN_FRAME_SYNC: + doAlignFrame(); + mEglThreadBlockVar.open(); + break; + case MSG_RELEASE: + doRelease(); + mEglThreadBlockVar.open(); + break; + } + } + + private void doAlignFrame() { + mInputSurfaceTexture.updateTexImage(); + mInputSurfaceTexture.getTransformMatrix(mTransformMatrix); + + MosaicRenderer.setWarping(true); + // Call preprocess to render it to low-res and high-res RGB textures. + MosaicRenderer.preprocess(mTransformMatrix); + // Now, transfer the textures from GPU to CPU memory for processing + MosaicRenderer.transferGPUtoCPU(); + MosaicRenderer.updateMatrix(); + MosaicRenderer.step(); + } + + private void doShowPreviewFrame() { + mInputSurfaceTexture.updateTexImage(); + mInputSurfaceTexture.getTransformMatrix(mTransformMatrix); + + MosaicRenderer.setWarping(false); + // Call preprocess to render it to low-res and high-res RGB textures. + MosaicRenderer.preprocess(mTransformMatrix); + MosaicRenderer.updateMatrix(); + MosaicRenderer.step(); + } + + private void doInit() { + mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init()); + MosaicRenderer.reset(mWidth, mHeight, mIsLandscape); + } + + private void doRelease() { + releaseSurfaceTexture(mInputSurfaceTexture); + mEglThread.quit(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void releaseSurfaceTexture(SurfaceTexture st) { + if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) { + st.release(); + } + } + + // Should be called from other thread. + public void sendMessageSync(int msg) { + mEglThreadBlockVar.close(); + sendEmptyMessage(msg); + mEglThreadBlockVar.block(); + } + } + + /** + * Constructor. + * + * @param tex The {@link SurfaceTexture} for the final UI output. + * @param w The width of the UI view. + * @param h The height of the UI view. + * @param isLandscape The UI orientation. {@code true} if in landscape, + * false if in portrait. + */ + public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape) { + mIsLandscape = isLandscape; + + mEglThread = new HandlerThread("PanoramaRealtimeRenderer"); + mEglThread.start(); + mHandler = new MyHandler(mEglThread.getLooper()); + mWidth = w; + mHeight = h; + + SurfaceTextureRenderer.FrameDrawer dummy = new SurfaceTextureRenderer.FrameDrawer() { + @Override + public void onDrawFrame(GL10 gl) { + // nothing, we have our draw functions. + } + }; + mSTRenderer = new SurfaceTextureRenderer(tex, mHandler, dummy); + + // We need to sync this because the generation of surface texture for input is + // done here and the client will continue with the assumption that the + // generation is completed. + mHandler.sendMessageSync(MyHandler.MSG_INIT_SYNC); + } + + public void release() { + mSTRenderer.release(); + mHandler.sendMessageSync(MyHandler.MSG_RELEASE); + } + + public void showPreviewFrameSync() { + mHandler.sendMessageSync(MyHandler.MSG_SHOW_PREVIEW_FRAME_SYNC); + mSTRenderer.draw(true); + } + + public void showPreviewFrame() { + mHandler.sendEmptyMessage(MyHandler.MSG_SHOW_PREVIEW_FRAME); + mSTRenderer.draw(false); + } + + public void alignFrameSync() { + mHandler.sendMessageSync(MyHandler.MSG_ALIGN_FRAME_SYNC); + mSTRenderer.draw(true); + } + + public SurfaceTexture getInputSurfaceTexture() { + return mInputSurfaceTexture; + } +} diff --git a/src/com/android/camera/MosaicRenderer.java b/src/com/android/camera/MosaicRenderer.java new file mode 100644 index 000000000..c50ca0d52 --- /dev/null +++ b/src/com/android/camera/MosaicRenderer.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2011 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.camera; + +/** + * The Java interface to JNI calls regarding mosaic preview rendering. + * + */ +public class MosaicRenderer +{ + static + { + System.loadLibrary("jni_mosaic"); + } + + /** + * Function to be called in onSurfaceCreated() to initialize + * the GL context, load and link the shaders and create the + * program. Returns a texture ID to be used for SurfaceTexture. + * + * @return textureID the texture ID of the newly generated texture to + * be assigned to the SurfaceTexture object. + */ + public static native int init(); + + /** + * Pass the drawing surface's width and height to initialize the + * renderer viewports and FBO dimensions. + * + * @param width width of the drawing surface in pixels. + * @param height height of the drawing surface in pixels. + * @param isLandscapeOrientation is the orientation of the activity layout in landscape. + */ + public static native void reset(int width, int height, boolean isLandscapeOrientation); + + /** + * Calling this function will render the SurfaceTexture to a new 2D texture + * using the provided STMatrix. + * + * @param stMatrix texture coordinate transform matrix obtained from the + * Surface texture + */ + public static native void preprocess(float[] stMatrix); + + /** + * This function calls glReadPixels to transfer both the low-res and high-res + * data from the GPU memory to the CPU memory for further processing by the + * mosaicing library. + */ + public static native void transferGPUtoCPU(); + + /** + * Function to be called in onDrawFrame() to update the screen with + * the new frame data. + */ + public static native void step(); + + /** + * Call this function when a new low-res frame has been processed by + * the mosaicing library. This will tell the renderer library to + * update its texture and warping transformation. Any calls to step() + * after this call will use the new image frame and transformation data. + */ + public static native void updateMatrix(); + + /** + * This function allows toggling between showing the input image data + * (without applying any warp) and the warped image data. For running + * the renderer as a viewfinder, we set the flag to false. To see the + * preview mosaic, we set the flag to true. + * + * @param flag boolean flag to set the warping to true or false. + */ + public static native void setWarping(boolean flag); +} diff --git a/src/com/android/camera/PanoProgressBar.java b/src/com/android/camera/PanoProgressBar.java new file mode 100644 index 000000000..8dfb3660b --- /dev/null +++ b/src/com/android/camera/PanoProgressBar.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2011 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.camera; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.widget.ImageView; + +class PanoProgressBar extends ImageView { + @SuppressWarnings("unused") + private static final String TAG = "PanoProgressBar"; + public static final int DIRECTION_NONE = 0; + public static final int DIRECTION_LEFT = 1; + public static final int DIRECTION_RIGHT = 2; + private float mProgress = 0; + private float mMaxProgress = 0; + private float mLeftMostProgress = 0; + private float mRightMostProgress = 0; + private float mProgressOffset = 0; + private float mIndicatorWidth = 0; + private int mDirection = 0; + private final Paint mBackgroundPaint = new Paint(); + private final Paint mDoneAreaPaint = new Paint(); + private final Paint mIndicatorPaint = new Paint(); + private float mWidth; + private float mHeight; + private RectF mDrawBounds; + private OnDirectionChangeListener mListener = null; + + public interface OnDirectionChangeListener { + public void onDirectionChange(int direction); + } + + public PanoProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + mDoneAreaPaint.setStyle(Paint.Style.FILL); + mDoneAreaPaint.setAlpha(0xff); + + mBackgroundPaint.setStyle(Paint.Style.FILL); + mBackgroundPaint.setAlpha(0xff); + + mIndicatorPaint.setStyle(Paint.Style.FILL); + mIndicatorPaint.setAlpha(0xff); + + mDrawBounds = new RectF(); + } + + public void setOnDirectionChangeListener(OnDirectionChangeListener l) { + mListener = l; + } + + private void setDirection(int direction) { + if (mDirection != direction) { + mDirection = direction; + if (mListener != null) { + mListener.onDirectionChange(mDirection); + } + invalidate(); + } + } + + public int getDirection() { + return mDirection; + } + + @Override + public void setBackgroundColor(int color) { + mBackgroundPaint.setColor(color); + invalidate(); + } + + public void setDoneColor(int color) { + mDoneAreaPaint.setColor(color); + invalidate(); + } + + public void setIndicatorColor(int color) { + mIndicatorPaint.setColor(color); + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + mHeight = h; + mDrawBounds.set(0, 0, mWidth, mHeight); + } + + public void setMaxProgress(int progress) { + mMaxProgress = progress; + } + + public void setIndicatorWidth(float w) { + mIndicatorWidth = w; + invalidate(); + } + + public void setRightIncreasing(boolean rightIncreasing) { + if (rightIncreasing) { + mLeftMostProgress = 0; + mRightMostProgress = 0; + mProgressOffset = 0; + setDirection(DIRECTION_RIGHT); + } else { + mLeftMostProgress = mWidth; + mRightMostProgress = mWidth; + mProgressOffset = mWidth; + setDirection(DIRECTION_LEFT); + } + invalidate(); + } + + public void setProgress(int progress) { + // The panning direction will be decided after user pan more than 10 degrees in one + // direction. + if (mDirection == DIRECTION_NONE) { + if (progress > 10) { + setRightIncreasing(true); + } else if (progress < -10) { + setRightIncreasing(false); + } + } + // mDirection might be modified by setRightIncreasing() above. Need to check again. + if (mDirection != DIRECTION_NONE) { + mProgress = progress * mWidth / mMaxProgress + mProgressOffset; + // value bounds. + mProgress = Math.min(mWidth, Math.max(0, mProgress)); + if (mDirection == DIRECTION_RIGHT) { + // The right most progress is adjusted. + mRightMostProgress = Math.max(mRightMostProgress, mProgress); + } + if (mDirection == DIRECTION_LEFT) { + // The left most progress is adjusted. + mLeftMostProgress = Math.min(mLeftMostProgress, mProgress); + } + invalidate(); + } + } + + public void reset() { + mProgress = 0; + mProgressOffset = 0; + setDirection(DIRECTION_NONE); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + // the background + canvas.drawRect(mDrawBounds, mBackgroundPaint); + if (mDirection != DIRECTION_NONE) { + // the progress area + canvas.drawRect(mLeftMostProgress, mDrawBounds.top, mRightMostProgress, + mDrawBounds.bottom, mDoneAreaPaint); + // the indication bar + float l; + float r; + if (mDirection == DIRECTION_RIGHT) { + l = Math.max(mProgress - mIndicatorWidth, 0f); + r = mProgress; + } else { + l = mProgress; + r = Math.min(mProgress + mIndicatorWidth, mWidth); + } + canvas.drawRect(l, mDrawBounds.top, r, mDrawBounds.bottom, mIndicatorPaint); + } + + // draw the mask image on the top for shaping. + super.onDraw(canvas); + } +} diff --git a/src/com/android/camera/PanoUtil.java b/src/com/android/camera/PanoUtil.java new file mode 100644 index 000000000..e50eaccc8 --- /dev/null +++ b/src/com/android/camera/PanoUtil.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011 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.camera; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class PanoUtil { + public static String createName(String format, long dateTaken) { + Date date = new Date(dateTaken); + SimpleDateFormat dateFormat = new SimpleDateFormat(format); + return dateFormat.format(date); + } + + // TODO: Add comments about the range of these two arguments. + public static double calculateDifferenceBetweenAngles(double firstAngle, + double secondAngle) { + double difference1 = (secondAngle - firstAngle) % 360; + if (difference1 < 0) { + difference1 += 360; + } + + double difference2 = (firstAngle - secondAngle) % 360; + if (difference2 < 0) { + difference2 += 360; + } + + return Math.min(difference1, difference2); + } + + public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) { + final int frameSize = width * height; + + for (int j = 0, ypd = 0; j < height; j += 4) { + int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; + for (int i = 0; i < width; i += 4, ypd++) { + int y = (0xff & (yuv420sp[j * width + i])) - 16; + if (y < 0) { + y = 0; + } + if ((i & 1) == 0) { + v = (0xff & yuv420sp[uvp++]) - 128; + u = (0xff & yuv420sp[uvp++]) - 128; + uvp += 2; // Skip the UV values for the 4 pixels skipped in between + } + int y1192 = 1192 * y; + int r = (y1192 + 1634 * v); + int g = (y1192 - 833 * v - 400 * u); + int b = (y1192 + 2066 * u); + + if (r < 0) { + r = 0; + } else if (r > 262143) { + r = 262143; + } + if (g < 0) { + g = 0; + } else if (g > 262143) { + g = 262143; + } + if (b < 0) { + b = 0; + } else if (b > 262143) { + b = 262143; + } + + rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | + ((b >> 10) & 0xff); + } + } + } +} diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java index ea0037db2..73840ab06 100644 --- a/src/com/android/camera/PhotoUI.java +++ b/src/com/android/camera/PhotoUI.java @@ -26,10 +26,7 @@ import android.graphics.SurfaceTexture; import android.graphics.drawable.ColorDrawable; import android.hardware.Camera; import android.hardware.Camera.Face; -import android.hardware.Camera.Size; import android.os.AsyncTask; -import android.os.Handler; -import android.os.Message; import android.util.Log; import android.view.Gravity; import android.view.TextureView; @@ -45,12 +42,11 @@ import android.widget.Toast; import com.android.camera.CameraPreference.OnPreferenceChangedListener; import com.android.camera.FocusOverlayManager.FocusUI; +import com.android.camera.ui.ModuleSwitcher; import com.android.camera.util.ApiHelper; import com.android.camera.ui.AbstractSettingPopup; import com.android.camera.ui.CameraControls; import com.android.camera.ui.CameraRootView; -import com.android.camera.ui.CameraSwitcher; -import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; import com.android.camera.ui.CountDownView; import com.android.camera.ui.CountDownView.OnCountDownFinishedListener; import com.android.camera.ui.FaceView; @@ -92,7 +88,7 @@ public class PhotoUI implements PieListener, private View mMenuButton; private PhotoMenu mMenu; - private CameraSwitcher mSwitcher; + private ModuleSwitcher mSwitcher; private CameraControls mCameraControls; private AlertDialog mLocationDialog; @@ -196,8 +192,8 @@ public class PhotoUI implements PieListener, initIndicators(); mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); - mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); - mSwitcher.setCurrentIndex(CameraSwitcher.PHOTO_MODULE_INDEX); + mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(ModuleSwitcher.PHOTO_MODULE_INDEX); mSwitcher.setSwitchListener(mActivity); mMenuButton = mRootView.findViewById(R.id.menu); if (ApiHelper.HAS_FACE_DETECTION) { diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java deleted file mode 100644 index 2bdace69c..000000000 --- a/src/com/android/camera/PreviewFrameLayout.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2009 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.camera; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.RelativeLayout; -import com.android.camera.util.ApiHelper; -import com.android.camera.ui.LayoutChangeHelper; -import com.android.camera.ui.LayoutChangeNotifier; -import com.android.camera.util.CameraUtil; -import com.android.camera2.R; - -/** - * A layout which handles the preview aspect ratio. - */ -public class PreviewFrameLayout extends RelativeLayout implements LayoutChangeNotifier { - - private static final String TAG = "CAM_preview"; - - /** A callback to be invoked when the preview frame's size changes. */ - public interface OnSizeChangedListener { - public void onSizeChanged(int width, int height); - } - - private double mAspectRatio; - private View mBorder; - private OnSizeChangedListener mListener; - private LayoutChangeHelper mLayoutChangeHelper; - - public PreviewFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - setAspectRatio(4.0 / 3.0); - mLayoutChangeHelper = new LayoutChangeHelper(this); - } - - @Override - protected void onFinishInflate() { - mBorder = findViewById(R.id.preview_border); - } - - public void setAspectRatio(double ratio) { - if (ratio <= 0.0) throw new IllegalArgumentException(); - - if (mAspectRatio != ratio) { - mAspectRatio = ratio; - requestLayout(); - } - } - - public void showBorder(boolean enabled) { - mBorder.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); - } - - public void fadeOutBorder() { - CameraUtil.fadeOut(mBorder); - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - int previewWidth = MeasureSpec.getSize(widthSpec); - int previewHeight = MeasureSpec.getSize(heightSpec); - - if (!ApiHelper.HAS_SURFACE_TEXTURE) { - // Get the padding of the border background. - int hPadding = getPaddingLeft() + getPaddingRight(); - int vPadding = getPaddingTop() + getPaddingBottom(); - - // Resize the preview frame with correct aspect ratio. - previewWidth -= hPadding; - previewHeight -= vPadding; - - boolean widthLonger = previewWidth > previewHeight; - int longSide = (widthLonger ? previewWidth : previewHeight); - int shortSide = (widthLonger ? previewHeight : previewWidth); - if (longSide > shortSide * mAspectRatio) { - longSide = (int) ((double) shortSide * mAspectRatio); - } else { - shortSide = (int) ((double) longSide / mAspectRatio); - } - if (widthLonger) { - previewWidth = longSide; - previewHeight = shortSide; - } else { - previewWidth = shortSide; - previewHeight = longSide; - } - - // Add the padding of the border. - previewWidth += hPadding; - previewHeight += vPadding; - } - - // Ask children to follow the new preview dimension. - super.onMeasure(MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY)); - } - - public void setOnSizeChangedListener(OnSizeChangedListener listener) { - mListener = listener; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - if (mListener != null) mListener.onSizeChanged(w, h); - } - - @Override - public void setOnLayoutChangeListener( - LayoutChangeNotifier.Listener listener) { - mLayoutChangeHelper.setOnLayoutChangeListener(listener); - } - - @SuppressLint("WrongCall") - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - // TODO: Suspicious call! - mLayoutChangeHelper.onLayout(changed, l, t, r, b); - } -} diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java index ad42b1aa0..88a7b5863 100644 --- a/src/com/android/camera/VideoUI.java +++ b/src/com/android/camera/VideoUI.java @@ -36,7 +36,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; -import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import android.widget.ImageView; import android.widget.LinearLayout; @@ -47,8 +46,7 @@ import com.android.camera.CameraPreference.OnPreferenceChangedListener; import com.android.camera.ui.AbstractSettingPopup; import com.android.camera.ui.CameraControls; import com.android.camera.ui.CameraRootView; -import com.android.camera.ui.CameraSwitcher; -import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.ModuleSwitcher; import com.android.camera.ui.PieRenderer; import com.android.camera.ui.RenderOverlay; import com.android.camera.ui.RotateLayout; @@ -74,7 +72,7 @@ public class VideoUI implements PieRenderer.PieListener, private View mReviewDoneButton; private View mReviewPlayButton; private ShutterButton mShutterButton; - private CameraSwitcher mSwitcher; + private ModuleSwitcher mSwitcher; private TextView mRecordingTimeView; private LinearLayout mLabelsLinearLayout; private View mTimeLapseLabel; @@ -172,9 +170,9 @@ public class VideoUI implements PieRenderer.PieListener, ((CameraRootView) mRootView).setDisplayChangeListener(this); mFlashOverlay = mRootView.findViewById(R.id.flash_overlay); mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); - mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); - mSwitcher.setCurrentIndex(CameraSwitcher.VIDEO_MODULE_INDEX); - mSwitcher.setSwitchListener((CameraSwitchListener) mActivity); + mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(ModuleSwitcher.VIDEO_MODULE_INDEX); + mSwitcher.setSwitchListener(mActivity); initializeMiscControls(); initializeControlByIntent(); initializeOverlay(); diff --git a/src/com/android/camera/WideAnglePanoramaController.java b/src/com/android/camera/WideAnglePanoramaController.java new file mode 100644 index 000000000..6ac7c511d --- /dev/null +++ b/src/com/android/camera/WideAnglePanoramaController.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +/** + * The interface that controls the wide angle panorama module. + */ +public interface WideAnglePanoramaController { + + public void onPreviewUIReady(); + + public void onPreviewUIDestroyed(); + + public void cancelHighResStitching(); + + public void onShutterButtonClick(); + + public void onPreviewUILayoutChange(int l, int t, int r, int b); +} diff --git a/src/com/android/camera/WideAnglePanoramaModule.java b/src/com/android/camera/WideAnglePanoramaModule.java new file mode 100644 index 000000000..1756e4c70 --- /dev/null +++ b/src/com/android/camera/WideAnglePanoramaModule.java @@ -0,0 +1,1071 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.graphics.YuvImage; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.location.Location; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.app.OrientationManager; +import com.android.camera.exif.ExifInterface; +import com.android.camera.ui.PopupManager; +import com.android.camera.util.CameraUtil; +import com.android.camera.util.UsageStatistics; +import com.android.camera2.R; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.TimeZone; + +/** + * Activity to handle panorama capturing. + */ +public class WideAnglePanoramaModule + implements CameraModule, WideAnglePanoramaController, + SurfaceTexture.OnFrameAvailableListener { + + public static final int DEFAULT_SWEEP_ANGLE = 160; + public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; + public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; + + private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; + private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2; + private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3; + private static final int MSG_CLEAR_SCREEN_DELAY = 4; + private static final int MSG_RESET_TO_PREVIEW = 5; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + @SuppressWarnings("unused") + private static final String TAG = "CAM_WidePanoModule"; + private static final int PREVIEW_STOPPED = 0; + private static final int PREVIEW_ACTIVE = 1; + public static final int CAPTURE_STATE_VIEWFINDER = 0; + public static final int CAPTURE_STATE_MOSAIC = 1; + + // The unit of speed is degrees per frame. + private static final float PANNING_SPEED_THRESHOLD = 2.5f; + private static final boolean DEBUG = false; + + private ContentResolver mContentResolver; + private WideAnglePanoramaUI mUI; + + private MosaicPreviewRenderer mMosaicPreviewRenderer; + private Object mRendererLock = new Object(); + private Object mWaitObject = new Object(); + + private String mPreparePreviewString; + private String mDialogTitle; + private String mDialogOkString; + private String mDialogPanoramaFailedString; + private String mDialogWaitingPreviousString; + + private int mPreviewUIWidth; + private int mPreviewUIHeight; + private boolean mUsingFrontCamera; + private int mCameraPreviewWidth; + private int mCameraPreviewHeight; + private int mCameraState; + private int mCaptureState; + private PowerManager.WakeLock mPartialWakeLock; + private MosaicFrameProcessor mMosaicFrameProcessor; + private boolean mMosaicFrameProcessorInitialized; + private AsyncTask mWaitProcessorTask; + private long mTimeTaken; + private Handler mMainHandler; + private SurfaceTexture mCameraTexture; + private boolean mThreadRunning; + private boolean mCancelComputation; + private float mHorizontalViewAngle; + private float mVerticalViewAngle; + + // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of + // getting a better image quality by the former. + private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY; + + private PanoOrientationEventListener mOrientationEventListener; + // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise + // respectively. + private int mDeviceOrientation; + private int mDeviceOrientationAtCapture; + private int mCameraOrientation; + private int mOrientationCompensation; + + private SoundClips.Player mSoundPlayer; + + private Runnable mOnFrameAvailableRunnable; + + private CameraActivity mActivity; + private View mRootView; + private CameraProxy mCameraDevice; + private boolean mPaused; + + private LocationManager mLocationManager; + private OrientationManager mOrientationManager; + private ComboPreferences mPreferences; + private boolean mMosaicPreviewConfigured; + + @Override + public void onPreviewUIReady() { + configMosaicPreview(); + } + + @Override + public void onPreviewUIDestroyed() { + + } + + private class MosaicJpeg { + public MosaicJpeg(byte[] data, int width, int height) { + this.data = data; + this.width = width; + this.height = height; + this.isValid = true; + } + + public MosaicJpeg() { + this.data = null; + this.width = 0; + this.height = 0; + this.isValid = false; + } + + public final byte[] data; + public final int width; + public final int height; + public final boolean isValid; + } + + private class PanoOrientationEventListener extends OrientationEventListener { + public PanoOrientationEventListener(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == ORIENTATION_UNKNOWN) return; + mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation); + // When the screen is unlocked, display rotation may change. Always + // calculate the up-to-date orientationCompensation. + int orientationCompensation = mDeviceOrientation + + CameraUtil.getDisplayRotation(mActivity) % 360; + if (mOrientationCompensation != orientationCompensation) { + mOrientationCompensation = orientationCompensation; + } + } + } + + @Override + public void init(CameraActivity activity, View parent) { + mActivity = activity; + mRootView = parent; + + mOrientationManager = new OrientationManager(activity); + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView); + mUI.setCaptureProgressOnDirectionChangeListener( + new PanoProgressBar.OnDirectionChangeListener() { + @Override + public void onDirectionChange(int direction) { + if (mCaptureState == CAPTURE_STATE_MOSAIC) { + mUI.showDirectionIndicators(direction); + } + } + }); + + mContentResolver = mActivity.getContentResolver(); + // This runs in UI thread. + mOnFrameAvailableRunnable = new Runnable() { + @Override + public void run() { + // Frames might still be available after the activity is paused. + // If we call onFrameAvailable after pausing, the GL thread will crash. + if (mPaused) return; + + MosaicPreviewRenderer renderer = null; + synchronized (mRendererLock) { + if (mMosaicPreviewRenderer == null) { + return; + } + renderer = mMosaicPreviewRenderer; + } + if (mRootView.getVisibility() != View.VISIBLE) { + renderer.showPreviewFrameSync(); + mRootView.setVisibility(View.VISIBLE); + } else { + if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { + renderer.showPreviewFrame(); + } else { + renderer.alignFrameSync(); + mMosaicFrameProcessor.processFrame(); + } + } + } + }; + + PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); + mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama"); + + mOrientationEventListener = new PanoOrientationEventListener(mActivity); + + mMosaicFrameProcessor = MosaicFrameProcessor.getInstance(); + + Resources appRes = mActivity.getResources(); + mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview); + mDialogTitle = appRes.getString(R.string.pano_dialog_title); + mDialogOkString = appRes.getString(R.string.dialog_ok); + mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed); + mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous); + + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mLocationManager = new LocationManager(mActivity, null); + + mMainHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOW_RES_FINAL_MOSAIC_READY: + onBackgroundThreadFinished(); + showFinalMosaic((Bitmap) msg.obj); + saveHighResMosaic(); + break; + case MSG_GENERATE_FINAL_MOSAIC_ERROR: + onBackgroundThreadFinished(); + if (mPaused) { + resetToPreviewIfPossible(); + } else { + mUI.showAlertDialog( + mDialogTitle, mDialogPanoramaFailedString, + mDialogOkString, new Runnable() { + @Override + public void run() { + resetToPreviewIfPossible(); + } + }); + } + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_END_DIALOG_RESET_TO_PREVIEW: + onBackgroundThreadFinished(); + resetToPreviewIfPossible(); + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_CLEAR_SCREEN_DELAY: + mActivity.getWindow().clearFlags(WindowManager.LayoutParams. + FLAG_KEEP_SCREEN_ON); + break; + case MSG_RESET_TO_PREVIEW: + resetToPreviewIfPossible(); + break; + } + } + }; + } + + @Override + public void onSwitchMode(boolean toCamera) { + if (toCamera) { + mUI.showUI(); + } else { + mUI.hideUI(); + } + } + + private void setupCamera() throws CameraHardwareException, CameraDisabledException { + openCamera(); + Parameters parameters = mCameraDevice.getParameters(); + setupCaptureParams(parameters); + configureCamera(parameters); + } + + private void releaseCamera() { + if (mCameraDevice != null) { + CameraHolder.instance().release(); + mCameraDevice = null; + mCameraState = PREVIEW_STOPPED; + } + } + + private void openCamera() throws CameraHardwareException, CameraDisabledException { + int cameraId = CameraHolder.instance().getBackCameraId(); + // If there is no back camera, use the first camera. Camera id starts + // from 0. Currently if a camera is not back facing, it is front facing. + // This is also forward compatible if we have a new facing other than + // back or front in the future. + if (cameraId == -1) cameraId = 0; + mCameraDevice = CameraUtil.openCamera(mActivity, cameraId); + mCameraOrientation = CameraUtil.getCameraOrientation(cameraId); + if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; + } + + private boolean findBestPreviewSize(List supportedSizes, boolean need4To3, + boolean needSmaller) { + int pixelsDiff = DEFAULT_CAPTURE_PIXELS; + boolean hasFound = false; + for (Size size : supportedSizes) { + int h = size.height; + int w = size.width; + // we only want 4:3 format. + int d = DEFAULT_CAPTURE_PIXELS - h * w; + if (needSmaller && d < 0) { // no bigger preview than 960x720. + continue; + } + if (need4To3 && (h * 4 != w * 3)) { + continue; + } + d = Math.abs(d); + if (d < pixelsDiff) { + mCameraPreviewWidth = w; + mCameraPreviewHeight = h; + pixelsDiff = d; + hasFound = true; + } + } + return hasFound; + } + + private void setupCaptureParams(Parameters parameters) { + List supportedSizes = parameters.getSupportedPreviewSizes(); + if (!findBestPreviewSize(supportedSizes, true, true)) { + Log.w(TAG, "No 4:3 ratio preview size supported."); + if (!findBestPreviewSize(supportedSizes, false, true)) { + Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); + findBestPreviewSize(supportedSizes, false, false); + } + } + Log.d(TAG, "camera preview h = " + + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth); + parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight); + + List frameRates = parameters.getSupportedPreviewFpsRange(); + int last = frameRates.size() - 1; + int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; + int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; + parameters.setPreviewFpsRange(minFps, maxFps); + Log.d(TAG, "preview fps: " + minFps + ", " + maxFps); + + List supportedFocusModes = parameters.getSupportedFocusModes(); + if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { + parameters.setFocusMode(mTargetFocusMode); + } else { + // Use the default focus mode and log a message + Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + + " becuase the mode is not supported."); + } + + parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE); + + mHorizontalViewAngle = parameters.getHorizontalViewAngle(); + mVerticalViewAngle = parameters.getVerticalViewAngle(); + } + + public int getPreviewBufSize() { + PixelFormat pixelInfo = new PixelFormat(); + PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); + // TODO: remove this extra 32 byte after the driver bug is fixed. + return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; + } + + private void configureCamera(Parameters parameters) { + mCameraDevice.setParameters(parameters); + } + + /** + * Configures the preview renderer according to the dimension defined by + * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}. + * Will stop the camera preview first. + */ + private void configMosaicPreview() { + if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0 + || mUI.getSurfaceTexture() == null) { + return; + } + + stopCameraPreview(); + synchronized (mRendererLock) { + if (mMosaicPreviewRenderer != null) { + mMosaicPreviewRenderer.release(); + } + mMosaicPreviewRenderer = null; + } + final boolean isLandscape = + (mActivity.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE); + + MosaicPreviewRenderer renderer = new MosaicPreviewRenderer( + mUI.getSurfaceTexture(), + mPreviewUIWidth, mPreviewUIHeight, isLandscape); + synchronized (mRendererLock) { + mMosaicPreviewRenderer = renderer; + mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); + + if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { + mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); + } + mRendererLock.notifyAll(); + } + mMosaicPreviewConfigured = true; + resetToPreviewIfPossible(); + } + + /** + * Receives the layout change event from the preview area. So we can + * initialize the mosaic preview renderer. + */ + @Override + public void onPreviewUILayoutChange(int l, int t, int r, int b) { + Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t)); + mPreviewUIWidth = r - l; + mPreviewUIHeight = b - t; + configMosaicPreview(); + } + + @Override + public void onFrameAvailable(SurfaceTexture surface) { + /* This function may be called by some random thread, + * so let's be safe and jump back to ui thread. + * No OpenGL calls can be done here. */ + mActivity.runOnUiThread(mOnFrameAvailableRunnable); + } + + public void startCapture() { + // Reset values so we can do this again. + mCancelComputation = false; + mTimeTaken = System.currentTimeMillis(); + mActivity.setSwipingEnabled(false); + mCaptureState = CAPTURE_STATE_MOSAIC; + mUI.onStartCapture(); + + mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { + @Override + public void onProgress(boolean isFinished, float panningRateX, float panningRateY, + float progressX, float progressY) { + float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle; + float accumulatedVerticalAngle = progressY * mVerticalViewAngle; + if (isFinished + || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE) + || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) { + stopCapture(false); + } else { + float panningRateXInDegree = panningRateX * mHorizontalViewAngle; + float panningRateYInDegree = panningRateY * mVerticalViewAngle; + mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree, + accumulatedHorizontalAngle, accumulatedVerticalAngle, + PANNING_SPEED_THRESHOLD); + } + } + }); + + mUI.resetCaptureProgress(); + // TODO: calculate the indicator width according to different devices to reflect the actual + // angle of view of the camera device. + mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE); + mUI.showCaptureProgress(); + mDeviceOrientationAtCapture = mDeviceOrientation; + keepScreenOn(); + // TODO: mActivity.getOrientationManager().lockOrientation(); + mOrientationManager.lockOrientation(); + int degrees = CameraUtil.getDisplayRotation(mActivity); + int cameraId = CameraHolder.instance().getBackCameraId(); + int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId); + mUI.setProgressOrientation(orientation); + } + + private void stopCapture(boolean aborted) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mUI.onStopCapture(); + + mMosaicFrameProcessor.setProgressListener(null); + stopCameraPreview(); + + mCameraTexture.setOnFrameAvailableListener(null); + + if (!aborted && !mThreadRunning) { + mUI.showWaitingDialog(mPreparePreviewString); + // Hide shutter button, shutter icon, etc when waiting for + // panorama to stitch + mUI.hideUI(); + runBackgroundThread(new Thread() { + @Override + public void run() { + MosaicJpeg jpeg = generateFinalMosaic(false); + + if (jpeg != null && jpeg.isValid) { + Bitmap bitmap = null; + bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); + } else { + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_END_DIALOG_RESET_TO_PREVIEW)); + } + } + }); + } + keepScreenOnAwhile(); + } + + @Override + public void onShutterButtonClick() { + // If mCameraTexture == null then GL setup is not finished yet. + // No buttons can be pressed. + if (mPaused || mThreadRunning || mCameraTexture == null) return; + // Since this button will stay on the screen when capturing, we need to check the state + // right now. + switch (mCaptureState) { + case CAPTURE_STATE_VIEWFINDER: + if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return; + mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); + startCapture(); + break; + case CAPTURE_STATE_MOSAIC: + mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); + stopCapture(false); + } + } + + public void reportProgress() { + mUI.resetSavingProgress(); + Thread t = new Thread() { + @Override + public void run() { + while (mThreadRunning) { + final int progress = mMosaicFrameProcessor.reportProgress( + true, mCancelComputation); + + try { + synchronized (mWaitObject) { + mWaitObject.wait(50); + } + } catch (InterruptedException e) { + throw new RuntimeException("Panorama reportProgress failed", e); + } + // Update the progress bar + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mUI.updateSavingProgress(progress); + } + }); + } + } + }; + t.start(); + } + + private int getCaptureOrientation() { + // The panorama image returned from the library is oriented based on the + // natural orientation of a camera. We need to set an orientation for the image + // in its EXIF header, so the image can be displayed correctly. + // The orientation is calculated from compensating the + // device orientation at capture and the camera orientation respective to + // the natural orientation of the device. + int orientation; + if (mUsingFrontCamera) { + // mCameraOrientation is negative with respect to the front facing camera. + // See document of android.hardware.Camera.Parameters.setRotation. + orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360; + } else { + orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; + } + return orientation; + } + + public void saveHighResMosaic() { + runBackgroundThread(new Thread() { + @Override + public void run() { + mPartialWakeLock.acquire(); + MosaicJpeg jpeg; + try { + jpeg = generateFinalMosaic(true); + } finally { + mPartialWakeLock.release(); + } + + if (jpeg == null) { // Cancelled by user. + mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW); + } else if (!jpeg.isValid) { // Error when generating mosaic. + mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); + } else { + int orientation = getCaptureOrientation(); + final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); + if (uri != null) { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mActivity.notifyNewMedia(uri); + } + }); + } + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW)); + } + } + }); + reportProgress(); + } + + private void runBackgroundThread(Thread thread) { + mThreadRunning = true; + thread.start(); + } + + private void onBackgroundThreadFinished() { + mThreadRunning = false; + mUI.dismissAllDialogs(); + } + + private void cancelHighResComputation() { + mCancelComputation = true; + synchronized (mWaitObject) { + mWaitObject.notify(); + } + } + + // This function will be called upon the first camera frame is available. + private void reset() { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + + mOrientationManager.unlockOrientation(); + mUI.reset(); + mActivity.setSwipingEnabled(true); + // Orientation change will trigger onLayoutChange->configMosaicPreview-> + // resetToPreview. Do not show the capture UI in film strip. + /* if (mActivity.mShowCameraAppView) { + mCaptureLayout.setVisibility(View.VISIBLE); */ + mUI.showPreviewUI(); + /*} else { + }*/ + mMosaicFrameProcessor.reset(); + } + + private void resetToPreviewIfPossible() { + if (!mMosaicFrameProcessorInitialized + || mUI.getSurfaceTexture() == null + || !mMosaicPreviewConfigured) { + return; + } + reset(); + if (!mPaused) startCameraPreview(); + } + + private void showFinalMosaic(Bitmap bitmap) { + mUI.showFinalMosaic(bitmap, getCaptureOrientation()); + } + + private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { + if (jpegData != null) { + String filename = PanoUtil.createName( + mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); + String filepath = Storage.generateFilepath(filename); + + Location loc = mLocationManager.getCurrentLocation(); + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(jpegData); + exif.addGpsDateTimeStampTag(mTimeTaken); + exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken, + TimeZone.getDefault()); + exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.getOrientationValueForRotation(orientation))); + writeLocation(loc, exif); + exif.writeExif(jpegData, filepath); + } catch (IOException e) { + Log.e(TAG, "Cannot set exif for " + filepath, e); + Storage.writeFile(filepath, jpegData); + } + int jpegLength = (int) (new File(filepath).length()); + return Storage.addImage(mContentResolver, filename, mTimeTaken, + loc, orientation, jpegLength, filepath, width, height); + } + return null; + } + + private static void writeLocation(Location location, ExifInterface exif) { + if (location == null) { + return; + } + exif.addGpsTags(location.getLatitude(), location.getLongitude()); + exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider())); + } + + private void clearMosaicFrameProcessorIfNeeded() { + if (!mPaused || mThreadRunning) return; + // Only clear the processor if it is initialized by this activity + // instance. Other activity instances may be using it. + if (mMosaicFrameProcessorInitialized) { + mMosaicFrameProcessor.clear(); + mMosaicFrameProcessorInitialized = false; + } + } + + private void initMosaicFrameProcessorIfNeeded() { + if (mPaused || mThreadRunning) { + return; + } + + mMosaicFrameProcessor.initialize( + mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize()); + mMosaicFrameProcessorInitialized = true; + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + if (mLocationManager != null) mLocationManager.recordLocation(false); + } + + @Override + public void onPauseAfterSuper() { + mOrientationEventListener.disable(); + if (mCameraDevice == null) { + // Camera open failed. Nothing should be done here. + return; + } + // Stop the capturing first. + if (mCaptureState == CAPTURE_STATE_MOSAIC) { + stopCapture(true); + reset(); + } + + releaseCamera(); + synchronized (mRendererLock) { + mCameraTexture = null; + + // The preview renderer might not have a chance to be initialized + // before onPause(). + if (mMosaicPreviewRenderer != null) { + mMosaicPreviewRenderer.release(); + mMosaicPreviewRenderer = null; + } + } + + clearMosaicFrameProcessorIfNeeded(); + if (mWaitProcessorTask != null) { + mWaitProcessorTask.cancel(true); + mWaitProcessorTask = null; + } + resetScreenOn(); + if (mSoundPlayer != null) { + mSoundPlayer.release(); + mSoundPlayer = null; + } + System.gc(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + mUI.onConfigurationChanged(newConfig, mThreadRunning); + } + + @Override + public void onOrientationChanged(int orientation) { + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + mOrientationEventListener.enable(); + + mCaptureState = CAPTURE_STATE_VIEWFINDER; + + try { + setupCamera(); + } catch (CameraHardwareException e) { + CameraUtil.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + CameraUtil.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + + // Set up sound playback for shutter button + mSoundPlayer = SoundClips.getPlayer(mActivity); + + // Check if another panorama instance is using the mosaic frame processor. + mUI.dismissAllDialogs(); + if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { + mUI.showWaitingDialog(mDialogWaitingPreviousString); + // If stitching is still going on, make sure switcher and shutter button + // are not showing + mUI.hideUI(); + mWaitProcessorTask = new WaitProcessorTask().execute(); + } else { + // Camera must be initialized before MosaicFrameProcessor is + // initialized. The preview size has to be decided by camera device. + initMosaicFrameProcessorIfNeeded(); + Point size = mUI.getPreviewAreaSize(); + mPreviewUIWidth = size.x; + mPreviewUIHeight = size.y; + configMosaicPreview(); + } + keepScreenOnAwhile(); + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get(mPreferences, + mContentResolver); + mLocationManager.recordLocation(recordLocation); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "PanoramaModule"); + } + + /** + * Generate the final mosaic image. + * + * @param highRes flag to indicate whether we want to get a high-res version. + * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation + * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there + * is an error in generating the final mosaic. + */ + public MosaicJpeg generateFinalMosaic(boolean highRes) { + int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); + if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { + return null; + } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { + return new MosaicJpeg(); + } + + byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); + if (imageData == null) { + Log.e(TAG, "getFinalMosaicNV21() returned null."); + return new MosaicJpeg(); + } + + int len = imageData.length - 8; + int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) + + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); + int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) + + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); + Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); + + if (width <= 0 || height <= 0) { + // TODO: pop up an error message indicating that the final result is not generated. + Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + + height); + return new MosaicJpeg(); + } + + YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); + try { + out.close(); + } catch (Exception e) { + Log.e(TAG, "Exception in storing final mosaic", e); + return new MosaicJpeg(); + } + return new MosaicJpeg(out.toByteArray(), width, height); + } + + private void startCameraPreview() { + if (mCameraDevice == null) { + // Camera open failed. Return. + return; + } + + if (mUI.getSurfaceTexture() == null) { + // UI is not ready. + return; + } + + // This works around a driver issue. startPreview may fail if + // stopPreview/setPreviewTexture/startPreview are called several times + // in a row. mCameraTexture can be null after pressing home during + // mosaic generation and coming back. Preview will be started later in + // onLayoutChange->configMosaicPreview. This also reduces the latency. + synchronized (mRendererLock) { + if (mCameraTexture == null) return; + + // If we're previewing already, stop the preview first (this will + // blank the screen). + if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); + + // Set the display orientation to 0, so that the underlying mosaic + // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight + // image data from SurfaceTexture. + mCameraDevice.setDisplayOrientation(0); + + mCameraTexture.setOnFrameAvailableListener(this); + mCameraDevice.setPreviewTexture(mCameraTexture); + } + mCameraDevice.startPreview(); + mCameraState = PREVIEW_ACTIVE; + } + + private void stopCameraPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + mCameraDevice.stopPreview(); + } + mCameraState = PREVIEW_STOPPED; + } + + @Override + public void onUserInteraction() { + if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); + } + + @Override + public boolean onBackPressed() { + // If panorama is generating low res or high res mosaic, ignore back + // key. So the activity will not be destroyed. + if (mThreadRunning) return true; + return false; + } + + private void resetScreenOn() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + private void keepScreenOn() { + mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private class WaitProcessorTask extends AsyncTask { + @Override + protected Void doInBackground(Void... params) { + synchronized (mMosaicFrameProcessor) { + while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { + try { + mMosaicFrameProcessor.wait(); + } catch (Exception e) { + // ignore + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + mWaitProcessorTask = null; + mUI.dismissAllDialogs(); + // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE); + initMosaicFrameProcessorIfNeeded(); + Point size = mUI.getPreviewAreaSize(); + mPreviewUIWidth = size.x; + mPreviewUIHeight = size.y; + configMosaicPreview(); + resetToPreviewIfPossible(); + } + } + + @Override + public void cancelHighResStitching() { + if (mPaused || mCameraTexture == null) return; + cancelHighResComputation(); + } + + @Override + public void onStop() { + } + + @Override + public void installIntentFilter() { + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + @Override + public void onSingleTapUp(View view, int x, int y) { + } + + @Override + public void onPreviewTextureCopied() { + } + + @Override + public void onCaptureTextureCopied() { + } + + @Override + public boolean updateStorageHintOnResume() { + return false; + } + + @Override + public void updateCameraAppView() { + } + + @Override + public void onShowSwitcherPopup() { + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // do nothing. + } +} diff --git a/src/com/android/camera/WideAnglePanoramaUI.java b/src/com/android/camera/WideAnglePanoramaUI.java new file mode 100644 index 000000000..0ce9d62dc --- /dev/null +++ b/src/com/android/camera/WideAnglePanoramaUI.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.camera.ui.CameraControls; +import com.android.camera.ui.CameraRootView; +import com.android.camera.ui.ModuleSwitcher; +import com.android.camera2.R; + +/** + * The UI of {@link WideAnglePanoramaModule}. + */ +public class WideAnglePanoramaUI implements + TextureView.SurfaceTextureListener, + ShutterButton.OnShutterButtonListener, + View.OnLayoutChangeListener { + + @SuppressWarnings("unused") + private static final String TAG = "CAM_WidePanoramaUI"; + + private CameraActivity mActivity; + private WideAnglePanoramaController mController; + + private ViewGroup mRootView; + private ModuleSwitcher mSwitcher; + private ViewGroup mPanoLayout; + private FrameLayout mCaptureLayout; + private View mReviewLayout; + private ImageView mReview; + private View mPreviewBorder; + private View mLeftIndicator; + private View mRightIndicator; + private View mCaptureIndicator; + private PanoProgressBar mCaptureProgressBar; + private PanoProgressBar mSavingProgressBar; + private TextView mTooFastPrompt; + private TextureView mTextureView; + private ShutterButton mShutterButton; + private CameraControls mCameraControls; + + private Matrix mProgressDirectionMatrix = new Matrix(); + private float[] mProgressAngle = new float[2]; + + private DialogHelper mDialogHelper; + + // Color definitions. + private int mIndicatorColor; + private int mIndicatorColorFast; + private int mReviewBackground; + private SurfaceTexture mSurfaceTexture; + + /** Constructor. */ + public WideAnglePanoramaUI( + CameraActivity activity, + WideAnglePanoramaController controller, + ViewGroup root) { + mActivity = activity; + mController = controller; + mRootView = root; + + createContentView(); + mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher); + Log.v(TAG, "setting CurrentIndex:" + ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX); + mSwitcher.setCurrentIndex(ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX); + mSwitcher.setSwitchListener(mActivity); + } + + public void onStartCapture() { + hideSwitcher(); + mShutterButton.setImageResource(R.drawable.btn_shutter_recording); + mCaptureIndicator.setVisibility(View.VISIBLE); + showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); + } + + public void showPreviewUI() { + mCaptureLayout.setVisibility(View.VISIBLE); + showUI(); + } + + public void onStopCapture() { + mCaptureIndicator.setVisibility(View.INVISIBLE); + hideTooFastIndication(); + hideDirectionIndicators(); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void hideUI() { + hideSwitcher(); + mCameraControls.setVisibility(View.INVISIBLE); + } + + public void showUI() { + showSwitcher(); + mCameraControls.setVisibility(View.VISIBLE); + } + + public void showSwitcher() { + mSwitcher.setVisibility(View.VISIBLE); + } + + public void setCaptureProgressOnDirectionChangeListener( + PanoProgressBar.OnDirectionChangeListener listener) { + mCaptureProgressBar.setOnDirectionChangeListener(listener); + } + + public void resetCaptureProgress() { + mCaptureProgressBar.reset(); + } + + public void setMaxCaptureProgress(int max) { + mCaptureProgressBar.setMaxProgress(max); + } + + public void showCaptureProgress() { + mCaptureProgressBar.setVisibility(View.VISIBLE); + } + + public void updateCaptureProgress( + float panningRateXInDegree, float panningRateYInDegree, + float progressHorizontalAngle, float progressVerticalAngle, + float maxPanningSpeed) { + + if ((Math.abs(panningRateXInDegree) > maxPanningSpeed) + || (Math.abs(panningRateYInDegree) > maxPanningSpeed)) { + showTooFastIndication(); + } else { + hideTooFastIndication(); + } + + // progressHorizontalAngle and progressVerticalAngle are relative to the + // camera. Convert them to UI direction. + mProgressAngle[0] = progressHorizontalAngle; + mProgressAngle[1] = progressVerticalAngle; + mProgressDirectionMatrix.mapPoints(mProgressAngle); + + int angleInMajorDirection = + (Math.abs(mProgressAngle[0]) > Math.abs(mProgressAngle[1])) + ? (int) mProgressAngle[0] + : (int) mProgressAngle[1]; + mCaptureProgressBar.setProgress((angleInMajorDirection)); + } + + public void setProgressOrientation(int orientation) { + mProgressDirectionMatrix.reset(); + mProgressDirectionMatrix.postRotate(orientation); + } + + public void showDirectionIndicators(int direction) { + switch (direction) { + case PanoProgressBar.DIRECTION_NONE: + mLeftIndicator.setVisibility(View.VISIBLE); + mRightIndicator.setVisibility(View.VISIBLE); + break; + case PanoProgressBar.DIRECTION_LEFT: + mLeftIndicator.setVisibility(View.VISIBLE); + mRightIndicator.setVisibility(View.INVISIBLE); + break; + case PanoProgressBar.DIRECTION_RIGHT: + mLeftIndicator.setVisibility(View.INVISIBLE); + mRightIndicator.setVisibility(View.VISIBLE); + break; + } + } + + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) { + mSurfaceTexture = surfaceTexture; + mController.onPreviewUIReady(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + mController.onPreviewUIDestroyed(); + mSurfaceTexture = null; + Log.d(TAG, "surfaceTexture is destroyed"); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + } + + private void hideDirectionIndicators() { + mLeftIndicator.setVisibility(View.INVISIBLE); + mRightIndicator.setVisibility(View.INVISIBLE); + } + + public Point getPreviewAreaSize() { + return new Point( + mTextureView.getWidth(), mTextureView.getHeight()); + } + + public void reset() { + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mReviewLayout.setVisibility(View.GONE); + mCaptureProgressBar.setVisibility(View.INVISIBLE); + } + + public void showFinalMosaic(Bitmap bitmap, int orientation) { + if (bitmap != null && orientation != 0) { + Matrix rotateMatrix = new Matrix(); + rotateMatrix.setRotate(orientation); + bitmap = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + rotateMatrix, false); + } + + mReview.setImageBitmap(bitmap); + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + + public void onConfigurationChanged( + Configuration newConfig, boolean threadRunning) { + Drawable lowResReview = null; + if (threadRunning) lowResReview = mReview.getDrawable(); + + // Change layout in response to configuration change + /* TODO (shkong):mCaptureLayout.setOrientation( + newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE + ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);*/ + LayoutInflater inflater = (LayoutInflater) + mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + mPanoLayout.removeView(mReviewLayout); + inflater.inflate(R.layout.pano_module_review, mPanoLayout); + + mPanoLayout.bringChildToFront(mCameraControls); + setViews(mActivity.getResources()); + if (threadRunning) { + mReview.setImageDrawable(lowResReview); + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + } + + public void resetSavingProgress() { + mSavingProgressBar.reset(); + mSavingProgressBar.setRightIncreasing(true); + } + + public void updateSavingProgress(int progress) { + mSavingProgressBar.setProgress(progress); + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + // Do nothing. + } + + @Override + public void onShutterButtonClick() { + mController.onShutterButtonClick(); + } + + @Override + public void onLayoutChange( + View v, int l, int t, int r, int b, + int oldl, int oldt, int oldr, int oldb) { + mController.onPreviewUILayoutChange(l, t, r, b); + } + + public void showAlertDialog( + String title, String failedString, + String OKString, Runnable runnable) { + mDialogHelper.showAlertDialog(title, failedString, OKString, runnable); + } + + public void showWaitingDialog(String title) { + mDialogHelper.showWaitingDialog(title); + } + + public void dismissAllDialogs() { + mDialogHelper.dismissAll(); + } + + private void createContentView() { + LayoutInflater inflator = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflator.inflate(R.layout.panorama_module, mRootView, true); + mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout); + Resources appRes = mActivity.getResources(); + mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); + mReviewBackground = appRes.getColor(R.color.review_background); + mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); + mDialogHelper = new DialogHelper(); + setViews(appRes); + } + + private void setViews(Resources appRes) { + mCaptureLayout = (FrameLayout) mRootView.findViewById(R.id.panorama_capture_layout); + // TODO (shkong): set display change listener properly. + ((CameraRootView) mRootView).setDisplayChangeListener(null); + mTextureView = (TextureView) mRootView.findViewById(R.id.pano_preview_textureview); + mTextureView.setSurfaceTextureListener(this); + mTextureView.addOnLayoutChangeListener(this); + mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls); + mCaptureProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar); + mCaptureProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); + mCaptureProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); + mCaptureProgressBar.setIndicatorColor(mIndicatorColor); + mCaptureProgressBar.setIndicatorWidth(20); + + mPreviewBorder = mCaptureLayout.findViewById(R.id.pano_preview_area_border); + + mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator); + mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator); + mLeftIndicator.setEnabled(false); + mRightIndicator.setEnabled(false); + mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview); + + mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar); + mSavingProgressBar.setIndicatorWidth(0); + mSavingProgressBar.setMaxProgress(100); + mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); + mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication)); + + mCaptureIndicator = mRootView.findViewById(R.id.pano_capture_indicator); + + mReviewLayout = mRootView.findViewById(R.id.pano_review_layout); + mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea); + mReview.setBackgroundColor(mReviewBackground); + View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + mController.cancelHighResStitching(); + } + }); + + mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(this); + } + + private void showTooFastIndication() { + mTooFastPrompt.setVisibility(View.VISIBLE); + // The PreviewArea also contains the border for "too fast" indication. + mPreviewBorder.setVisibility(View.VISIBLE); + mCaptureProgressBar.setIndicatorColor(mIndicatorColorFast); + mLeftIndicator.setEnabled(true); + mRightIndicator.setEnabled(true); + } + + private void hideTooFastIndication() { + mTooFastPrompt.setVisibility(View.GONE); + mPreviewBorder.setVisibility(View.INVISIBLE); + mCaptureProgressBar.setIndicatorColor(mIndicatorColor); + mLeftIndicator.setEnabled(false); + mRightIndicator.setEnabled(false); + } + + private class DialogHelper { + private ProgressDialog mProgressDialog; + private AlertDialog mAlertDialog; + + DialogHelper() { + mProgressDialog = null; + mAlertDialog = null; + } + + public void dismissAll() { + if (mAlertDialog != null) { + mAlertDialog.dismiss(); + mAlertDialog = null; + } + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + + public void showAlertDialog( + CharSequence title, CharSequence message, + CharSequence buttonMessage, final Runnable buttonRunnable) { + dismissAll(); + mAlertDialog = (new AlertDialog.Builder(mActivity)) + .setTitle(title) + .setMessage(message) + .setNeutralButton(buttonMessage, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + buttonRunnable.run(); + } + }) + .show(); + } + + public void showWaitingDialog(CharSequence message) { + dismissAll(); + mProgressDialog = ProgressDialog.show(mActivity, null, message, true, false); + } + } + + private static class FlipBitmapDrawable extends BitmapDrawable { + + public FlipBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + int cx = bounds.centerX(); + int cy = bounds.centerY(); + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.rotate(180, cx, cy); + super.draw(canvas); + canvas.restore(); + } + } +} diff --git a/src/com/android/camera/app/AppManagerFactory.java b/src/com/android/camera/app/AppManagerFactory.java new file mode 100644 index 000000000..9c047aa55 --- /dev/null +++ b/src/com/android/camera/app/AppManagerFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.app; + +import android.app.Application; +import android.content.Context; + +/** + * A singleton class which provides application level utility + * classes. + */ +public class AppManagerFactory { + + private static AppManagerFactory sFactory; + + public static synchronized AppManagerFactory getInstance(Context ctx) { + if (sFactory == null) { + sFactory = new AppManagerFactory(ctx); + } + return sFactory; + } + + private PanoramaStitchingManager mPanoramaStitchingManager; + + /** No public constructor. */ + private AppManagerFactory(Context ctx) { + mPanoramaStitchingManager = new PanoramaStitchingManager(ctx); + } + + public PanoramaStitchingManager getPanoramaStitchingManager() { + return mPanoramaStitchingManager; + } +} diff --git a/src/com/android/camera/app/CameraApp.java b/src/com/android/camera/app/CameraApp.java index e4da41461..2e61fa99a 100644 --- a/src/com/android/camera/app/CameraApp.java +++ b/src/com/android/camera/app/CameraApp.java @@ -28,3 +28,4 @@ public class CameraApp extends Application { CameraUtil.initialize(this); } } + diff --git a/src/com/android/camera/app/OrientationManager.java b/src/com/android/camera/app/OrientationManager.java index 412be3024..7bf924218 100644 --- a/src/com/android/camera/app/OrientationManager.java +++ b/src/com/android/camera/app/OrientationManager.java @@ -1,5 +1,20 @@ -package com.android.camera.app; +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.camera.app; import android.app.Activity; import android.content.ContentResolver; @@ -13,8 +28,8 @@ import android.view.Surface; import com.android.camera.util.ApiHelper; -public class OrientationManager implements OrientationSource { - private static final String TAG = "OrientationManager"; +public class OrientationManager { + private static final String TAG = "CAM_OrientationManager"; // Orientation hysteresis amount used in rounding, in degrees private static final int ORIENTATION_HYSTERESIS = 5; @@ -112,12 +127,10 @@ public class OrientationManager implements OrientationSource { } } - @Override public int getDisplayRotation() { return getDisplayRotation(mActivity); } - @Override public int getCompensation() { return 0; } @@ -148,4 +161,4 @@ public class OrientationManager implements OrientationSource { } return 0; } -} \ No newline at end of file +} diff --git a/src/com/android/camera/app/OrientationSource.java b/src/com/android/camera/app/OrientationSource.java deleted file mode 100644 index 57dcfeb40..000000000 --- a/src/com/android/camera/app/OrientationSource.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.camera.app; - -public interface OrientationSource { - public int getDisplayRotation(); - public int getCompensation(); -} diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java index 48f24e4f2..75d08428b 100644 --- a/src/com/android/camera/ui/CameraRootView.java +++ b/src/com/android/camera/ui/CameraRootView.java @@ -77,7 +77,9 @@ public class CameraRootView extends FrameLayout { @Override public void onDisplayChanged(int arg0) { - mListener.onDisplayChanged(); + if (mListener != null) { + mListener.onDisplayChanged(); + } } @Override diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java deleted file mode 100644 index aaa9cdac8..000000000 --- a/src/com/android/camera/ui/CameraSwitcher.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * 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.camera.ui; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.app.Activity; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.widget.FrameLayout.LayoutParams; -import android.widget.LinearLayout; - -import com.android.camera.util.CameraUtil; -import com.android.camera.util.PhotoSphereHelper; -import com.android.camera.util.UsageStatistics; -import com.android.camera2.R; -import com.android.camera.util.ApiHelper; - -public class CameraSwitcher extends RotateImageView - implements OnClickListener, OnTouchListener { - - @SuppressWarnings("unused") - private static final String TAG = "CAM_Switcher"; - private static final int SWITCHER_POPUP_ANIM_DURATION = 200; - - public static final int PHOTO_MODULE_INDEX = 0; - public static final int VIDEO_MODULE_INDEX = 1; - public static final int LIGHTCYCLE_MODULE_INDEX = 2; - private static final int[] DRAW_IDS = { - R.drawable.ic_switch_camera, - R.drawable.ic_switch_video, - R.drawable.ic_switch_photosphere, - }; - - public interface CameraSwitchListener { - public void onCameraSelected(int i); - - public void onShowSwitcherPopup(); - } - - private CameraSwitchListener mListener; - private int mCurrentIndex; - private int[] mModuleIds; - private int[] mDrawIds; - private int mItemSize; - private View mPopup; - private View mParent; - private boolean mShowingPopup; - private boolean mNeedsAnimationSetup; - private Drawable mIndicator; - - private float mTranslationX = 0; - private float mTranslationY = 0; - - private AnimatorListener mHideAnimationListener; - private AnimatorListener mShowAnimationListener; - - public CameraSwitcher(Context context) { - super(context); - init(context); - } - - public CameraSwitcher(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - private void init(Context context) { - mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); - setOnClickListener(this); - mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); - initializeDrawables(context); - } - - public void initializeDrawables(Context context) { - int numDrawIds = DRAW_IDS.length; - - if (!PhotoSphereHelper.hasLightCycleCapture(context)) { - --numDrawIds; - } - - int[] drawids = new int[numDrawIds]; - int[] moduleids = new int[numDrawIds]; - int ix = 0; - for (int i = 0; i < DRAW_IDS.length; i++) { - if (i == LIGHTCYCLE_MODULE_INDEX && !PhotoSphereHelper.hasLightCycleCapture(context)) { - continue; // not enabled, so don't add to UI - } - moduleids[ix] = i; - drawids[ix++] = DRAW_IDS[i]; - } - setIds(moduleids, drawids); - } - - public void setIds(int[] moduleids, int[] drawids) { - mDrawIds = drawids; - mModuleIds = moduleids; - } - - public void setCurrentIndex(int i) { - mCurrentIndex = i; - setImageResource(mDrawIds[i]); - } - - public void setSwitchListener(CameraSwitchListener l) { - mListener = l; - } - - @Override - public void onClick(View v) { - showSwitcher(); - mListener.onShowSwitcherPopup(); - } - - private void onCameraSelected(int ix) { - hidePopup(); - if ((ix != mCurrentIndex) && (mListener != null)) { - UsageStatistics.onEvent("CameraModeSwitch", null, null); - UsageStatistics.setPendingTransitionCause( - UsageStatistics.TRANSITION_MENU_TAP); - setCurrentIndex(ix); - mListener.onCameraSelected(mModuleIds[ix]); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - mIndicator.setBounds(getDrawable().getBounds()); - mIndicator.draw(canvas); - } - - private void initPopup() { - mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup, - (ViewGroup) getParent()); - LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content); - mPopup = content; - // Set the gravity of the popup, so that it shows up at the right - // position - // on screen - LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams()); - lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher) - .getLayoutParams()).gravity; - mPopup.setLayoutParams(lp); - - mPopup.setVisibility(View.INVISIBLE); - mNeedsAnimationSetup = true; - for (int i = mDrawIds.length - 1; i >= 0; i--) { - RotateImageView item = new RotateImageView(getContext()); - item.setImageResource(mDrawIds[i]); - item.setBackgroundResource(R.drawable.bg_pressed); - final int index = i; - item.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (showsPopup()) { - onCameraSelected(index); - } - } - }); - switch (mDrawIds[i]) { - case R.drawable.ic_switch_camera: - item.setContentDescription(getContext().getResources().getString( - R.string.accessibility_switch_to_camera)); - break; - case R.drawable.ic_switch_video: - item.setContentDescription(getContext().getResources().getString( - R.string.accessibility_switch_to_video)); - break; - case R.drawable.ic_switch_photosphere: - item.setContentDescription(getContext().getResources().getString( - R.string.accessibility_switch_to_photo_sphere)); - break; - default: - break; - } - content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); - } - mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST)); - } - - public boolean showsPopup() { - return mShowingPopup; - } - - public boolean isInsidePopup(MotionEvent evt) { - if (!showsPopup()) { - return false; - } - int topLeft[] = new int[2]; - mPopup.getLocationOnScreen(topLeft); - int left = topLeft[0]; - int top = topLeft[1]; - int bottom = top + mPopup.getHeight(); - int right = left + mPopup.getWidth(); - return evt.getX() >= left && evt.getX() < right - && evt.getY() >= top && evt.getY() < bottom; - } - - private void hidePopup() { - mShowingPopup = false; - setVisibility(View.VISIBLE); - if (mPopup != null && !animateHidePopup()) { - mPopup.setVisibility(View.INVISIBLE); - } - mParent.setOnTouchListener(null); - } - - @Override - public void onConfigurationChanged(Configuration config) { - if (showsPopup()) { - ((ViewGroup) mParent).removeView(mPopup); - mPopup = null; - initPopup(); - mPopup.setVisibility(View.VISIBLE); - } - } - - private void showSwitcher() { - mShowingPopup = true; - if (mPopup == null) { - initPopup(); - } - layoutPopup(); - mPopup.setVisibility(View.VISIBLE); - if (!animateShowPopup()) { - setVisibility(View.INVISIBLE); - } - mParent.setOnTouchListener(this); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - closePopup(); - return true; - } - - public void closePopup() { - if (showsPopup()) { - hidePopup(); - } - } - - @Override - public void setOrientation(int degree, boolean animate) { - super.setOrientation(degree, animate); - ViewGroup content = (ViewGroup) mPopup; - if (content == null) { - return; - } - for (int i = 0; i < content.getChildCount(); i++) { - RotateImageView iv = (RotateImageView) content.getChildAt(i); - iv.setOrientation(degree, animate); - } - } - - private void layoutPopup() { - int orientation = CameraUtil.getDisplayRotation((Activity) getContext()); - int w = mPopup.getMeasuredWidth(); - int h = mPopup.getMeasuredHeight(); - if (orientation == 0) { - mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom()); - mTranslationX = 0; - mTranslationY = h / 3; - } else if (orientation == 90) { - mTranslationX = w / 3; - mTranslationY = -h / 3; - mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h); - } else if (orientation == 180) { - mTranslationX = -w / 3; - mTranslationY = -h / 3; - mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h); - } else { - mTranslationX = -w / 3; - mTranslationY = h - getHeight(); - mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom()); - } - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mPopup != null) { - layoutPopup(); - } - } - - private void popupAnimationSetup() { - if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { - return; - } - layoutPopup(); - mPopup.setScaleX(0.3f); - mPopup.setScaleY(0.3f); - mPopup.setTranslationX(mTranslationX); - mPopup.setTranslationY(mTranslationY); - mNeedsAnimationSetup = false; - } - - private boolean animateHidePopup() { - if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { - return false; - } - if (mHideAnimationListener == null) { - mHideAnimationListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Verify that we weren't canceled - if (!showsPopup() && mPopup != null) { - mPopup.setVisibility(View.INVISIBLE); - ((ViewGroup) mParent).removeView(mPopup); - mPopup = null; - } - } - }; - } - mPopup.animate() - .alpha(0f) - .scaleX(0.3f).scaleY(0.3f) - .translationX(mTranslationX) - .translationY(mTranslationY) - .setDuration(SWITCHER_POPUP_ANIM_DURATION) - .setListener(mHideAnimationListener); - animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION) - .setListener(null); - return true; - } - - private boolean animateShowPopup() { - if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { - return false; - } - if (mNeedsAnimationSetup) { - popupAnimationSetup(); - } - if (mShowAnimationListener == null) { - mShowAnimationListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Verify that we weren't canceled - if (showsPopup()) { - setVisibility(View.INVISIBLE); - // request layout to make sure popup is laid out - // correctly on ICS - mPopup.requestLayout(); - } - } - }; - } - mPopup.animate() - .alpha(1f) - .scaleX(1f).scaleY(1f) - .translationX(0) - .translationY(0) - .setDuration(SWITCHER_POPUP_ANIM_DURATION) - .setListener(null); - animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION) - .setListener(mShowAnimationListener); - return true; - } -} diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java deleted file mode 100644 index ef4eb6a7a..000000000 --- a/src/com/android/camera/ui/LayoutChangeHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.camera.ui; - -import android.view.View; - -public class LayoutChangeHelper implements LayoutChangeNotifier { - private LayoutChangeNotifier.Listener mListener; - private boolean mFirstTimeLayout; - private View mView; - - public LayoutChangeHelper(View v) { - mView = v; - mFirstTimeLayout = true; - } - - @Override - public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) { - mListener = listener; - } - - public void onLayout(boolean changed, int l, int t, int r, int b) { - if (mListener == null) return; - if (mFirstTimeLayout || changed) { - mFirstTimeLayout = false; - mListener.onLayoutChange(mView, l, t, r, b); - } - } -} diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java deleted file mode 100644 index 6261d34f6..000000000 --- a/src/com/android/camera/ui/LayoutChangeNotifier.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.camera.ui; - -import android.view.View; - -public interface LayoutChangeNotifier { - public interface Listener { - // Invoked only when the layout has changed or it is the first layout. - public void onLayoutChange(View v, int l, int t, int r, int b); - } - - public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener); -} diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java deleted file mode 100644 index 6e118fc3a..000000000 --- a/src/com/android/camera/ui/LayoutNotifyView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.camera.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -/* - * Customized view to support onLayoutChange() at or before API 10. - */ -public class LayoutNotifyView extends View implements LayoutChangeNotifier { - private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this); - - public LayoutNotifyView(Context context) { - super(context); - } - - public LayoutNotifyView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setOnLayoutChangeListener( - LayoutChangeNotifier.Listener listener) { - mLayoutChangeHelper.setOnLayoutChangeListener(listener); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - mLayoutChangeHelper.onLayout(changed, l, t, r, b); - } -} diff --git a/src/com/android/camera/ui/ModuleSwitcher.java b/src/com/android/camera/ui/ModuleSwitcher.java new file mode 100644 index 000000000..5eb316c7f --- /dev/null +++ b/src/com/android/camera/ui/ModuleSwitcher.java @@ -0,0 +1,392 @@ +/* + * 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.camera.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.FrameLayout.LayoutParams; +import android.widget.LinearLayout; + +import com.android.camera.util.ApiHelper; +import com.android.camera.util.CameraUtil; +import com.android.camera.util.PhotoSphereHelper; +import com.android.camera.util.UsageStatistics; +import com.android.camera2.R; + +public class ModuleSwitcher extends RotateImageView + implements OnClickListener, OnTouchListener { + + @SuppressWarnings("unused") + private static final String TAG = "CAM_Switcher"; + private static final int SWITCHER_POPUP_ANIM_DURATION = 200; + + public static final int PHOTO_MODULE_INDEX = 0; + public static final int VIDEO_MODULE_INDEX = 1; + public static final int WIDE_ANGLE_PANO_MODULE_INDEX = 2; + public static final int LIGHTCYCLE_MODULE_INDEX = 3; + private static final int[] DRAW_IDS = { + R.drawable.ic_switch_camera, + R.drawable.ic_switch_video, + R.drawable.ic_switch_pan, + R.drawable.ic_switch_photosphere, + }; + + public interface ModuleSwitchListener { + public void onModuleSelected(int i); + + public void onShowSwitcherPopup(); + } + + private ModuleSwitchListener mListener; + private int mCurrentIndex; + private int[] mModuleIds; + private int[] mDrawIds; + private int mItemSize; + private View mPopup; + private View mParent; + private boolean mShowingPopup; + private boolean mNeedsAnimationSetup; + private Drawable mIndicator; + + private float mTranslationX = 0; + private float mTranslationY = 0; + + private AnimatorListener mHideAnimationListener; + private AnimatorListener mShowAnimationListener; + + public ModuleSwitcher(Context context) { + super(context); + init(context); + } + + public ModuleSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); + setOnClickListener(this); + mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); + initializeDrawables(context); + } + + public void initializeDrawables(Context context) { + int numDrawIds = DRAW_IDS.length; + + if (!PhotoSphereHelper.hasLightCycleCapture(context)) { + --numDrawIds; + } + + int[] drawids = new int[numDrawIds]; + int[] moduleids = new int[numDrawIds]; + int ix = 0; + for (int i = 0; i < DRAW_IDS.length; i++) { + if (i == LIGHTCYCLE_MODULE_INDEX && !PhotoSphereHelper.hasLightCycleCapture(context)) { + continue; // not enabled, so don't add to UI + } + moduleids[ix] = i; + drawids[ix++] = DRAW_IDS[i]; + } + setIds(moduleids, drawids); + } + + public void setIds(int[] moduleids, int[] drawids) { + mDrawIds = drawids; + mModuleIds = moduleids; + } + + public void setCurrentIndex(int i) { + mCurrentIndex = i; + setImageResource(mDrawIds[i]); + } + + public void setSwitchListener(ModuleSwitchListener l) { + mListener = l; + } + + @Override + public void onClick(View v) { + showSwitcher(); + mListener.onShowSwitcherPopup(); + } + + private void onModuleSelected(int ix) { + hidePopup(); + if ((ix != mCurrentIndex) && (mListener != null)) { + UsageStatistics.onEvent("CameraModeSwitch", null, null); + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_MENU_TAP); + setCurrentIndex(ix); + mListener.onModuleSelected(mModuleIds[ix]); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mIndicator.setBounds(getDrawable().getBounds()); + mIndicator.draw(canvas); + } + + private void initPopup() { + mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup, + (ViewGroup) getParent()); + LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content); + mPopup = content; + // Set the gravity of the popup, so that it shows up at the right + // position + // on screen + LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams()); + lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher) + .getLayoutParams()).gravity; + mPopup.setLayoutParams(lp); + + mPopup.setVisibility(View.INVISIBLE); + mNeedsAnimationSetup = true; + for (int i = mDrawIds.length - 1; i >= 0; i--) { + RotateImageView item = new RotateImageView(getContext()); + item.setImageResource(mDrawIds[i]); + item.setBackgroundResource(R.drawable.bg_pressed); + final int index = i; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (showsPopup()) { + onModuleSelected(index); + } + } + }); + switch (mDrawIds[i]) { + case R.drawable.ic_switch_camera: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_camera)); + break; + case R.drawable.ic_switch_video: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_video)); + break; + case R.drawable.ic_switch_pan: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_panorama)); + break; + case R.drawable.ic_switch_photosphere: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_photo_sphere)); + break; + default: + break; + } + content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); + } + mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST)); + } + + public boolean showsPopup() { + return mShowingPopup; + } + + public boolean isInsidePopup(MotionEvent evt) { + if (!showsPopup()) { + return false; + } + int topLeft[] = new int[2]; + mPopup.getLocationOnScreen(topLeft); + int left = topLeft[0]; + int top = topLeft[1]; + int bottom = top + mPopup.getHeight(); + int right = left + mPopup.getWidth(); + return evt.getX() >= left && evt.getX() < right + && evt.getY() >= top && evt.getY() < bottom; + } + + private void hidePopup() { + mShowingPopup = false; + setVisibility(View.VISIBLE); + if (mPopup != null && !animateHidePopup()) { + mPopup.setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(null); + } + + @Override + public void onConfigurationChanged(Configuration config) { + if (showsPopup()) { + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + initPopup(); + mPopup.setVisibility(View.VISIBLE); + } + } + + private void showSwitcher() { + mShowingPopup = true; + if (mPopup == null) { + initPopup(); + } + layoutPopup(); + mPopup.setVisibility(View.VISIBLE); + if (!animateShowPopup()) { + setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + closePopup(); + return true; + } + + public void closePopup() { + if (showsPopup()) { + hidePopup(); + } + } + + @Override + public void setOrientation(int degree, boolean animate) { + super.setOrientation(degree, animate); + ViewGroup content = (ViewGroup) mPopup; + if (content == null) { + return; + } + for (int i = 0; i < content.getChildCount(); i++) { + RotateImageView iv = (RotateImageView) content.getChildAt(i); + iv.setOrientation(degree, animate); + } + } + + private void layoutPopup() { + int orientation = CameraUtil.getDisplayRotation((Activity) getContext()); + int w = mPopup.getMeasuredWidth(); + int h = mPopup.getMeasuredHeight(); + if (orientation == 0) { + mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom()); + mTranslationX = 0; + mTranslationY = h / 3; + } else if (orientation == 90) { + mTranslationX = w / 3; + mTranslationY = -h / 3; + mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h); + } else if (orientation == 180) { + mTranslationX = -w / 3; + mTranslationY = -h / 3; + mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h); + } else { + mTranslationX = -w / 3; + mTranslationY = h - getHeight(); + mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom()); + } + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mPopup != null) { + layoutPopup(); + } + } + + private void popupAnimationSetup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return; + } + layoutPopup(); + mPopup.setScaleX(0.3f); + mPopup.setScaleY(0.3f); + mPopup.setTranslationX(mTranslationX); + mPopup.setTranslationY(mTranslationY); + mNeedsAnimationSetup = false; + } + + private boolean animateHidePopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mHideAnimationListener == null) { + mHideAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (!showsPopup() && mPopup != null) { + mPopup.setVisibility(View.INVISIBLE); + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + } + } + }; + } + mPopup.animate() + .alpha(0f) + .scaleX(0.3f).scaleY(0.3f) + .translationX(mTranslationX) + .translationY(mTranslationY) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mHideAnimationListener); + animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + return true; + } + + private boolean animateShowPopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mNeedsAnimationSetup) { + popupAnimationSetup(); + } + if (mShowAnimationListener == null) { + mShowAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (showsPopup()) { + setVisibility(View.INVISIBLE); + // request layout to make sure popup is laid out + // correctly on ICS + mPopup.requestLayout(); + } + } + }; + } + mPopup.animate() + .alpha(1f) + .scaleX(1f).scaleY(1f) + .translationX(0) + .translationY(0) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mShowAnimationListener); + return true; + } +} diff --git a/src_pd/com/android/camera/PanoramaStitchingManager.java b/src_pd/com/android/camera/PanoramaStitchingManager.java deleted file mode 100644 index 48f67ced2..000000000 --- a/src_pd/com/android/camera/PanoramaStitchingManager.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.camera; - -import android.content.Context; -import android.net.Uri; - -class PanoramaStitchingManager implements ImageTaskManager { - - public PanoramaStitchingManager(Context ctx) { - } - - @Override - public void addTaskListener(TaskListener l) { - // do nothing. - } - - @Override - public void removeTaskListener(TaskListener l) { - // do nothing. - } - - @Override - public int getTaskProgress(Uri uri) { - return -1; - } -} diff --git a/src_pd/com/android/camera/app/PanoramaStitchingManager.java b/src_pd/com/android/camera/app/PanoramaStitchingManager.java new file mode 100644 index 000000000..9d6e7964d --- /dev/null +++ b/src_pd/com/android/camera/app/PanoramaStitchingManager.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.app; + +import android.content.Context; +import android.net.Uri; + +import com.android.camera.ImageTaskManager; + +public class PanoramaStitchingManager implements ImageTaskManager { + + public PanoramaStitchingManager(Context ctx) { + } + + @Override + public void addTaskListener(TaskListener l) { + // do nothing. + } + + @Override + public void removeTaskListener(TaskListener l) { + // do nothing. + } + + @Override + public int getTaskProgress(Uri uri) { + return -1; + } +} -- cgit v1.2.3