From 8872c23e739de38d74f04a8c852ebb5199c905f6 Mon Sep 17 00:00:00 2001 From: Michael Kolb Date: Tue, 29 Jan 2013 10:33:22 -0800 Subject: Move Camera Java/Native source into Gallery2 Change-Id: I968efe4d656e88a7760d3c0044f65b4adac2ddd1 --- src/com/android/camera/ActivityBase.java | 642 +++++ src/com/android/camera/CameraActivity.java | 437 +++ src/com/android/camera/CameraBackupAgent.java | 32 + .../android/camera/CameraButtonIntentReceiver.java | 53 + .../android/camera/CameraDisabledException.java | 24 + src/com/android/camera/CameraErrorCallback.java | 35 + .../android/camera/CameraHardwareException.java | 28 + src/com/android/camera/CameraHolder.java | 298 +++ src/com/android/camera/CameraManager.java | 490 ++++ src/com/android/camera/CameraModule.java | 75 + src/com/android/camera/CameraPreference.java | 61 + src/com/android/camera/CameraScreenNail.java | 497 ++++ src/com/android/camera/CameraSettings.java | 582 ++++ src/com/android/camera/CaptureAnimManager.java | 146 + src/com/android/camera/ComboPreferences.java | 332 +++ .../android/camera/CountDownTimerPreference.java | 51 + src/com/android/camera/DisableCameraReceiver.java | 85 + src/com/android/camera/EffectsRecorder.java | 1239 +++++++++ src/com/android/camera/Exif.java | 74 + src/com/android/camera/FocusOverlayManager.java | 560 ++++ src/com/android/camera/IconListPreference.java | 115 + src/com/android/camera/IntArray.java | 45 + src/com/android/camera/ListPreference.java | 181 ++ src/com/android/camera/LocationManager.java | 181 ++ src/com/android/camera/MediaSaver.java | 149 ++ src/com/android/camera/Mosaic.java | 206 ++ src/com/android/camera/MosaicFrameProcessor.java | 236 ++ src/com/android/camera/MosaicPreviewRenderer.java | 264 ++ src/com/android/camera/MosaicRenderer.java | 89 + src/com/android/camera/OnClickAttr.java | 31 + src/com/android/camera/OnScreenHint.java | 188 ++ src/com/android/camera/PanoProgressBar.java | 188 ++ src/com/android/camera/PanoUtil.java | 86 + src/com/android/camera/PanoramaModule.java | 1312 +++++++++ src/com/android/camera/PhotoController.java | 225 ++ src/com/android/camera/PhotoModule.java | 2481 +++++++++++++++++ src/com/android/camera/PieController.java | 191 ++ src/com/android/camera/PreferenceGroup.java | 79 + src/com/android/camera/PreferenceInflater.java | 108 + src/com/android/camera/PreviewFrameLayout.java | 144 + src/com/android/camera/PreviewGestures.java | 329 +++ src/com/android/camera/ProxyLauncher.java | 46 + .../android/camera/RecordLocationPreference.java | 58 + src/com/android/camera/RotateDialogController.java | 168 ++ src/com/android/camera/SecureCameraActivity.java | 23 + src/com/android/camera/ShutterButton.java | 130 + src/com/android/camera/SoundClips.java | 193 ++ src/com/android/camera/StaticBitmapScreenNail.java | 32 + src/com/android/camera/Storage.java | 172 ++ src/com/android/camera/SwitchAnimManager.java | 146 + src/com/android/camera/Thumbnail.java | 68 + src/com/android/camera/Util.java | 776 ++++++ src/com/android/camera/VideoController.java | 186 ++ src/com/android/camera/VideoModule.java | 2816 ++++++++++++++++++++ src/com/android/camera/drawable/TextDrawable.java | 84 + .../android/camera/ui/AbstractSettingPopup.java | 44 + src/com/android/camera/ui/CameraSwitcher.java | 293 ++ src/com/android/camera/ui/CheckedLinearLayout.java | 60 + src/com/android/camera/ui/CountDownView.java | 131 + src/com/android/camera/ui/EffectSettingPopup.java | 214 ++ src/com/android/camera/ui/ExpandedGridView.java | 36 + src/com/android/camera/ui/FaceView.java | 217 ++ src/com/android/camera/ui/FocusIndicator.java | 24 + .../android/camera/ui/InLineSettingCheckBox.java | 83 + src/com/android/camera/ui/InLineSettingItem.java | 94 + src/com/android/camera/ui/InLineSettingMenu.java | 78 + src/com/android/camera/ui/LayoutChangeHelper.java | 43 + .../android/camera/ui/LayoutChangeNotifier.java | 28 + src/com/android/camera/ui/LayoutNotifyView.java | 48 + .../android/camera/ui/ListPrefSettingPopup.java | 127 + src/com/android/camera/ui/MoreSettingPopup.java | 203 ++ .../camera/ui/OnIndicatorEventListener.java | 25 + src/com/android/camera/ui/OverlayRenderer.java | 95 + src/com/android/camera/ui/PieItem.java | 203 ++ src/com/android/camera/ui/PieRenderer.java | 825 ++++++ src/com/android/camera/ui/PopupManager.java | 66 + src/com/android/camera/ui/PreviewSurfaceView.java | 50 + src/com/android/camera/ui/RenderOverlay.java | 165 ++ src/com/android/camera/ui/Rotatable.java | 22 + src/com/android/camera/ui/RotateImageView.java | 176 ++ src/com/android/camera/ui/RotateLayout.java | 203 ++ src/com/android/camera/ui/RotateTextToast.java | 59 + src/com/android/camera/ui/Switch.java | 505 ++++ src/com/android/camera/ui/TimeIntervalPopup.java | 164 ++ src/com/android/camera/ui/TimerSettingPopup.java | 153 ++ src/com/android/camera/ui/TwoStateImageView.java | 55 + src/com/android/camera/ui/ZoomRenderer.java | 158 ++ 87 files changed, 22114 insertions(+) create mode 100644 src/com/android/camera/ActivityBase.java create mode 100644 src/com/android/camera/CameraActivity.java create mode 100644 src/com/android/camera/CameraBackupAgent.java create mode 100644 src/com/android/camera/CameraButtonIntentReceiver.java create mode 100644 src/com/android/camera/CameraDisabledException.java create mode 100644 src/com/android/camera/CameraErrorCallback.java create mode 100644 src/com/android/camera/CameraHardwareException.java create mode 100644 src/com/android/camera/CameraHolder.java create mode 100644 src/com/android/camera/CameraManager.java create mode 100644 src/com/android/camera/CameraModule.java create mode 100644 src/com/android/camera/CameraPreference.java create mode 100644 src/com/android/camera/CameraScreenNail.java create mode 100644 src/com/android/camera/CameraSettings.java create mode 100644 src/com/android/camera/CaptureAnimManager.java create mode 100644 src/com/android/camera/ComboPreferences.java create mode 100644 src/com/android/camera/CountDownTimerPreference.java create mode 100644 src/com/android/camera/DisableCameraReceiver.java create mode 100644 src/com/android/camera/EffectsRecorder.java create mode 100644 src/com/android/camera/Exif.java create mode 100644 src/com/android/camera/FocusOverlayManager.java create mode 100644 src/com/android/camera/IconListPreference.java create mode 100644 src/com/android/camera/IntArray.java create mode 100644 src/com/android/camera/ListPreference.java create mode 100644 src/com/android/camera/LocationManager.java create mode 100644 src/com/android/camera/MediaSaver.java 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/OnClickAttr.java create mode 100644 src/com/android/camera/OnScreenHint.java create mode 100644 src/com/android/camera/PanoProgressBar.java create mode 100644 src/com/android/camera/PanoUtil.java create mode 100644 src/com/android/camera/PanoramaModule.java create mode 100644 src/com/android/camera/PhotoController.java create mode 100644 src/com/android/camera/PhotoModule.java create mode 100644 src/com/android/camera/PieController.java create mode 100644 src/com/android/camera/PreferenceGroup.java create mode 100644 src/com/android/camera/PreferenceInflater.java create mode 100644 src/com/android/camera/PreviewFrameLayout.java create mode 100644 src/com/android/camera/PreviewGestures.java create mode 100644 src/com/android/camera/ProxyLauncher.java create mode 100644 src/com/android/camera/RecordLocationPreference.java create mode 100644 src/com/android/camera/RotateDialogController.java create mode 100644 src/com/android/camera/SecureCameraActivity.java create mode 100755 src/com/android/camera/ShutterButton.java create mode 100644 src/com/android/camera/SoundClips.java create mode 100644 src/com/android/camera/StaticBitmapScreenNail.java create mode 100644 src/com/android/camera/Storage.java create mode 100644 src/com/android/camera/SwitchAnimManager.java create mode 100644 src/com/android/camera/Thumbnail.java create mode 100644 src/com/android/camera/Util.java create mode 100644 src/com/android/camera/VideoController.java create mode 100644 src/com/android/camera/VideoModule.java create mode 100644 src/com/android/camera/drawable/TextDrawable.java create mode 100644 src/com/android/camera/ui/AbstractSettingPopup.java create mode 100644 src/com/android/camera/ui/CameraSwitcher.java create mode 100644 src/com/android/camera/ui/CheckedLinearLayout.java create mode 100644 src/com/android/camera/ui/CountDownView.java create mode 100644 src/com/android/camera/ui/EffectSettingPopup.java create mode 100644 src/com/android/camera/ui/ExpandedGridView.java create mode 100644 src/com/android/camera/ui/FaceView.java create mode 100644 src/com/android/camera/ui/FocusIndicator.java create mode 100644 src/com/android/camera/ui/InLineSettingCheckBox.java create mode 100644 src/com/android/camera/ui/InLineSettingItem.java create mode 100644 src/com/android/camera/ui/InLineSettingMenu.java create mode 100644 src/com/android/camera/ui/LayoutChangeHelper.java create mode 100644 src/com/android/camera/ui/LayoutChangeNotifier.java create mode 100644 src/com/android/camera/ui/LayoutNotifyView.java create mode 100644 src/com/android/camera/ui/ListPrefSettingPopup.java create mode 100644 src/com/android/camera/ui/MoreSettingPopup.java create mode 100644 src/com/android/camera/ui/OnIndicatorEventListener.java create mode 100644 src/com/android/camera/ui/OverlayRenderer.java create mode 100644 src/com/android/camera/ui/PieItem.java create mode 100644 src/com/android/camera/ui/PieRenderer.java create mode 100644 src/com/android/camera/ui/PopupManager.java create mode 100644 src/com/android/camera/ui/PreviewSurfaceView.java create mode 100644 src/com/android/camera/ui/RenderOverlay.java create mode 100644 src/com/android/camera/ui/Rotatable.java create mode 100644 src/com/android/camera/ui/RotateImageView.java create mode 100644 src/com/android/camera/ui/RotateLayout.java create mode 100644 src/com/android/camera/ui/RotateTextToast.java create mode 100644 src/com/android/camera/ui/Switch.java create mode 100644 src/com/android/camera/ui/TimeIntervalPopup.java create mode 100644 src/com/android/camera/ui/TimerSettingPopup.java create mode 100644 src/com/android/camera/ui/TwoStateImageView.java create mode 100644 src/com/android/camera/ui/ZoomRenderer.java (limited to 'src/com') diff --git a/src/com/android/camera/ActivityBase.java b/src/com/android/camera/ActivityBase.java new file mode 100644 index 000000000..4e4143ef8 --- /dev/null +++ b/src/com/android/camera/ActivityBase.java @@ -0,0 +1,642 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.hardware.Camera.Parameters; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; + +import com.android.camera.ui.LayoutChangeNotifier; +import com.android.camera.ui.PopupManager; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AppBridge; +import com.android.gallery3d.app.FilmstripPage; +import com.android.gallery3d.app.GalleryActionBar; +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.ui.ScreenNail; +import com.android.gallery3d.util.MediaSetUtils; + +/** + * Superclass of camera activity. + */ +public abstract class ActivityBase extends AbstractGalleryActivity + implements LayoutChangeNotifier.Listener { + + private static final String TAG = "ActivityBase"; + private static final int CAMERA_APP_VIEW_TOGGLE_TIME = 100; // milliseconds + private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE = + "android.media.action.STILL_IMAGE_CAMERA_SECURE"; + public static final String ACTION_IMAGE_CAPTURE_SECURE = + "android.media.action.IMAGE_CAPTURE_SECURE"; + // The intent extra for camera from secure lock screen. True if the gallery + // should only show newly captured pictures. sSecureAlbumId does not + // increment. This is used when switching between camera, camcorder, and + // panorama. If the extra is not set, it is in the normal camera mode. + public static final String SECURE_CAMERA_EXTRA = "secure_camera"; + + private int mResultCodeForTesting; + private Intent mResultDataForTesting; + private OnScreenHint mStorageHint; + private View mSingleTapArea; + + protected boolean mOpenCameraFail; + protected boolean mCameraDisabled; + protected CameraManager.CameraProxy mCameraDevice; + protected Parameters mParameters; + // The activity is paused. The classes that extend this class should set + // mPaused the first thing in onResume/onPause. + protected boolean mPaused; + protected GalleryActionBar mActionBar; + + // multiple cameras support + protected int mNumberOfCameras; + protected int mCameraId; + // The activity is going to switch to the specified camera id. This is + // needed because texture copy is done in GL thread. -1 means camera is not + // switching. + protected int mPendingSwitchCameraId = -1; + + protected MyAppBridge mAppBridge; + protected ScreenNail mCameraScreenNail; // This shows camera preview. + // The view containing only camera related widgets like control panel, + // indicator bar, focus indicator and etc. + protected View mCameraAppView; + protected boolean mShowCameraAppView = true; + private Animation mCameraAppViewFadeIn; + private Animation mCameraAppViewFadeOut; + // Secure album id. This should be incremented every time the camera is + // launched from the secure lock screen. The id should be the same when + // switching between camera, camcorder, and panorama. + protected static int sSecureAlbumId; + // True if the camera is started from secure lock screen. + protected boolean mSecureCamera; + private static boolean sFirstStartAfterScreenOn = true; + + private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD; + private static final int UPDATE_STORAGE_HINT = 0; + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_STORAGE_HINT: + updateStorageHint(); + return; + } + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_MEDIA_MOUNTED) + || action.equals(Intent.ACTION_MEDIA_UNMOUNTED) + || action.equals(Intent.ACTION_MEDIA_CHECKING) + || action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { + updateStorageSpaceAndHint(); + } + } + }; + + // close activity when screen turns off + private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + finish(); + } + }; + + private static BroadcastReceiver sScreenOffReceiver; + private static class ScreenOffReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + sFirstStartAfterScreenOn = true; + } + } + + public static boolean isFirstStartAfterScreenOn() { + return sFirstStartAfterScreenOn; + } + + public static void resetFirstStartAfterScreenOn() { + sFirstStartAfterScreenOn = false; + } + + protected class CameraOpenThread extends Thread { + @Override + public void run() { + try { + mCameraDevice = Util.openCamera(ActivityBase.this, mCameraId); + mParameters = mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + mOpenCameraFail = true; + } catch (CameraDisabledException e) { + mCameraDisabled = true; + } + } + } + + @Override + public void onCreate(Bundle icicle) { + super.disableToggleStatusBar(); + // Set a theme with action bar. It is not specified in manifest because + // we want to hide it by default. setTheme must happen before + // setContentView. + // + // This must be set before we call super.onCreate(), where the window's + // background is removed. + setTheme(R.style.Theme_Gallery); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (ApiHelper.HAS_ACTION_BAR) { + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + } else { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + + // Check if this is in the secure camera mode. + Intent intent = getIntent(); + String action = intent.getAction(); + if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)) { + mSecureCamera = true; + // Use a new album when this is started from the lock screen. + sSecureAlbumId++; + } else if (ACTION_IMAGE_CAPTURE_SECURE.equals(action)) { + mSecureCamera = true; + } else { + mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false); + } + if (mSecureCamera) { + IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenOffReceiver, filter); + if (sScreenOffReceiver == null) { + sScreenOffReceiver = new ScreenOffReceiver(); + getApplicationContext().registerReceiver(sScreenOffReceiver, filter); + } + } + super.onCreate(icicle); + } + + public boolean isPanoramaActivity() { + return false; + } + + @Override + protected void onResume() { + super.onResume(); + + installIntentFilter(); + if (updateStorageHintOnResume()) { + updateStorageSpace(); + mHandler.sendEmptyMessageDelayed(UPDATE_STORAGE_HINT, 200); + } + } + + @Override + protected void onPause() { + super.onPause(); + + if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } + + unregisterReceiver(mReceiver); + } + + @Override + public void setContentView(int layoutResID) { + super.setContentView(layoutResID); + // getActionBar() should be after setContentView + mActionBar = new GalleryActionBar(this); + mActionBar.hide(); + } + + @Override + public boolean onSearchRequested() { + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Prevent software keyboard or voice search from showing up. + if (keyCode == KeyEvent.KEYCODE_SEARCH + || keyCode == KeyEvent.KEYCODE_MENU) { + if (event.isLongPress()) return true; + } + if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraAppView) { + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraAppView) { + return true; + } + return super.onKeyUp(keyCode, event); + } + + protected void setResultEx(int resultCode) { + mResultCodeForTesting = resultCode; + setResult(resultCode); + } + + protected void setResultEx(int resultCode, Intent data) { + mResultCodeForTesting = resultCode; + mResultDataForTesting = data; + setResult(resultCode, data); + } + + public int getResultCode() { + return mResultCodeForTesting; + } + + public Intent getResultData() { + return mResultDataForTesting; + } + + @Override + protected void onDestroy() { + PopupManager.removeInstance(this); + if (mSecureCamera) unregisterReceiver(mScreenOffReceiver); + super.onDestroy(); + } + + protected void installIntentFilter() { + // install an intent filter to receive SD card related events. + IntentFilter intentFilter = + new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); + intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING); + intentFilter.addDataScheme("file"); + registerReceiver(mReceiver, intentFilter); + } + + protected void updateStorageSpace() { + mStorageSpace = Storage.getAvailableSpace(); + } + + protected long getStorageSpace() { + return mStorageSpace; + } + + protected void updateStorageSpaceAndHint() { + updateStorageSpace(); + updateStorageHint(mStorageSpace); + } + + protected void updateStorageHint() { + updateStorageHint(mStorageSpace); + } + + protected boolean updateStorageHintOnResume() { + return true; + } + + protected void updateStorageHint(long storageSpace) { + String message = null; + if (storageSpace == Storage.UNAVAILABLE) { + message = getString(R.string.no_storage); + } else if (storageSpace == Storage.PREPARING) { + message = getString(R.string.preparing_sd); + } else if (storageSpace == Storage.UNKNOWN_SIZE) { + message = getString(R.string.access_sd_fail); + } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) { + message = getString(R.string.spaceIsLow_content); + } + + if (message != null) { + if (mStorageHint == null) { + mStorageHint = OnScreenHint.makeText(this, message); + } else { + mStorageHint.setText(message); + } + mStorageHint.show(); + } else if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } + } + + protected void gotoGallery() { + // Move the next picture with capture animation. "1" means next. + mAppBridge.switchWithCaptureAnimation(1); + } + + // Call this after setContentView. + public ScreenNail createCameraScreenNail(boolean getPictures) { + mCameraAppView = findViewById(R.id.camera_app_root); + Bundle data = new Bundle(); + String path; + if (getPictures) { + if (mSecureCamera) { + path = "/secure/all/" + sSecureAlbumId; + } else { + path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID; + } + } else { + path = "/local/all/0"; // Use 0 so gallery does not show anything. + } + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path); + data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera); + + // Send an AppBridge to gallery to enable the camera preview. + if (mAppBridge != null) { + mCameraScreenNail.recycle(); + } + mAppBridge = new MyAppBridge(); + data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge); + if (getStateManager().getStateCount() == 0) { + getStateManager().startState(FilmstripPage.class, data); + } else { + getStateManager().switchState(getStateManager().getTopState(), + FilmstripPage.class, data); + } + mCameraScreenNail = mAppBridge.getCameraScreenNail(); + return mCameraScreenNail; + } + + // Call this after setContentView. + protected ScreenNail reuseCameraScreenNail(boolean getPictures) { + mCameraAppView = findViewById(R.id.camera_app_root); + Bundle data = new Bundle(); + String path; + if (getPictures) { + if (mSecureCamera) { + path = "/secure/all/" + sSecureAlbumId; + } else { + path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID; + } + } else { + path = "/local/all/0"; // Use 0 so gallery does not show anything. + } + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path); + data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera); + + // Send an AppBridge to gallery to enable the camera preview. + if (mAppBridge == null) { + mAppBridge = new MyAppBridge(); + } + data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge); + if (getStateManager().getStateCount() == 0) { + getStateManager().startState(FilmstripPage.class, data); + } + mCameraScreenNail = mAppBridge.getCameraScreenNail(); + return mCameraScreenNail; + } + + private class HideCameraAppView implements Animation.AnimationListener { + @Override + public void onAnimationEnd(Animation animation) { + // We cannot set this as GONE because we want to receive the + // onLayoutChange() callback even when we are invisible. + mCameraAppView.setVisibility(View.INVISIBLE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + protected void updateCameraAppView() { + // Initialize the animation. + if (mCameraAppViewFadeIn == null) { + mCameraAppViewFadeIn = new AlphaAnimation(0f, 1f); + mCameraAppViewFadeIn.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME); + mCameraAppViewFadeIn.setInterpolator(new DecelerateInterpolator()); + + mCameraAppViewFadeOut = new AlphaAnimation(1f, 0f); + mCameraAppViewFadeOut.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME); + mCameraAppViewFadeOut.setInterpolator(new DecelerateInterpolator()); + mCameraAppViewFadeOut.setAnimationListener(new HideCameraAppView()); + } + + if (mShowCameraAppView) { + mCameraAppView.setVisibility(View.VISIBLE); + // The "transparent region" is not recomputed when a sibling of + // SurfaceView changes visibility (unless it involves GONE). It's + // been broken since 1.0. Call requestLayout to work around it. + mCameraAppView.requestLayout(); + mCameraAppView.startAnimation(mCameraAppViewFadeIn); + } else { + mCameraAppView.startAnimation(mCameraAppViewFadeOut); + } + } + + protected void onFullScreenChanged(boolean full) { + if (mShowCameraAppView == full) return; + mShowCameraAppView = full; + if (mPaused || isFinishing()) return; + updateCameraAppView(); + } + + @Override + public GalleryActionBar getGalleryActionBar() { + return mActionBar; + } + + // Preview frame layout has changed. + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom) { + if (mAppBridge == null) return; + + int width = right - left; + int height = bottom - top; + if (ApiHelper.HAS_SURFACE_TEXTURE) { + CameraScreenNail screenNail = (CameraScreenNail) mCameraScreenNail; + if (Util.getDisplayRotation(this) % 180 == 0) { + screenNail.setPreviewFrameLayoutSize(width, height); + } else { + // Swap the width and height. Camera screen nail draw() is based on + // natural orientation, not the view system orientation. + screenNail.setPreviewFrameLayoutSize(height, width); + } + notifyScreenNailChanged(); + } + } + + protected void setSingleTapUpListener(View singleTapArea) { + mSingleTapArea = singleTapArea; + } + + private boolean onSingleTapUp(int x, int y) { + // Ignore if listener is null or the camera control is invisible. + if (mSingleTapArea == null || !mShowCameraAppView) return false; + + int[] relativeLocation = Util.getRelativeLocation((View) getGLRoot(), + mSingleTapArea); + x -= relativeLocation[0]; + y -= relativeLocation[1]; + if (x >= 0 && x < mSingleTapArea.getWidth() && y >= 0 + && y < mSingleTapArea.getHeight()) { + onSingleTapUp(mSingleTapArea, x, y); + return true; + } + return false; + } + + protected void onSingleTapUp(View view, int x, int y) { + } + + public void setSwipingEnabled(boolean enabled) { + mAppBridge.setSwipingEnabled(enabled); + } + + public void notifyScreenNailChanged() { + mAppBridge.notifyScreenNailChanged(); + } + + protected void onPreviewTextureCopied() { + } + + protected void onCaptureTextureCopied() { + } + + protected void addSecureAlbumItemIfNeeded(boolean isVideo, Uri uri) { + if (mSecureCamera) { + int id = Integer.parseInt(uri.getLastPathSegment()); + mAppBridge.addSecureAlbumItem(isVideo, id); + } + } + + public boolean isSecureCamera() { + return mSecureCamera; + } + + ////////////////////////////////////////////////////////////////////////// + // The is the communication interface between the Camera Application and + // the Gallery PhotoPage. + ////////////////////////////////////////////////////////////////////////// + + class MyAppBridge extends AppBridge implements CameraScreenNail.Listener { + @SuppressWarnings("hiding") + private ScreenNail mCameraScreenNail; + private Server mServer; + + @Override + public ScreenNail attachScreenNail() { + if (mCameraScreenNail == null) { + if (ApiHelper.HAS_SURFACE_TEXTURE) { + mCameraScreenNail = new CameraScreenNail(this); + } else { + Bitmap b = BitmapFactory.decodeResource(getResources(), + R.drawable.wallpaper_picker_preview); + mCameraScreenNail = new StaticBitmapScreenNail(b); + } + } + return mCameraScreenNail; + } + + @Override + public void detachScreenNail() { + mCameraScreenNail = null; + } + + public ScreenNail getCameraScreenNail() { + return mCameraScreenNail; + } + + // Return true if the tap is consumed. + @Override + public boolean onSingleTapUp(int x, int y) { + return ActivityBase.this.onSingleTapUp(x, y); + } + + // This is used to notify that the screen nail will be drawn in full screen + // or not in next draw() call. + @Override + public void onFullScreenChanged(boolean full) { + ActivityBase.this.onFullScreenChanged(full); + } + + @Override + public void requestRender() { + getGLRoot().requestRenderForced(); + } + + @Override + public void onPreviewTextureCopied() { + ActivityBase.this.onPreviewTextureCopied(); + } + + @Override + public void onCaptureTextureCopied() { + ActivityBase.this.onCaptureTextureCopied(); + } + + @Override + public void setServer(Server s) { + mServer = s; + } + + @Override + public boolean isPanorama() { + return ActivityBase.this.isPanoramaActivity(); + } + + @Override + public boolean isStaticCamera() { + return !ApiHelper.HAS_SURFACE_TEXTURE; + } + + public void addSecureAlbumItem(boolean isVideo, int id) { + if (mServer != null) mServer.addSecureAlbumItem(isVideo, id); + } + + private void setCameraRelativeFrame(Rect frame) { + if (mServer != null) mServer.setCameraRelativeFrame(frame); + } + + private void switchWithCaptureAnimation(int offset) { + if (mServer != null) mServer.switchWithCaptureAnimation(offset); + } + + private void setSwipingEnabled(boolean enabled) { + if (mServer != null) mServer.setSwipingEnabled(enabled); + } + + private void notifyScreenNailChanged() { + if (mServer != null) mServer.notifyScreenNailChanged(); + } + } +} diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java new file mode 100644 index 000000000..d79832b7d --- /dev/null +++ b/src/com/android/camera/CameraActivity.java @@ -0,0 +1,437 @@ +/* + * 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; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.camera.ui.CameraSwitcher; +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; + +public class CameraActivity extends ActivityBase + implements CameraSwitcher.CameraSwitchListener { + public static final int PHOTO_MODULE_INDEX = 0; + public static final int VIDEO_MODULE_INDEX = 1; + public static final int PANORAMA_MODULE_INDEX = 2; + public static final int LIGHTCYCLE_MODULE_INDEX = 3; + + CameraModule mCurrentModule; + private FrameLayout mFrame; + private ShutterButton mShutter; + private CameraSwitcher mSwitcher; + private View mShutterSwitcher; + private View mControlsBackground; + private Drawable[] mDrawables; + private int mCurrentModuleIndex; + private MotionEvent mDown; + + private MyOrientationEventListener mOrientationListener; + // The degrees of the device rotated clockwise from its natural orientation. + private int mLastRawOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + + private static final String TAG = "CAM_activity"; + + 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 + }; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + setContentView(R.layout.camera_main); + mFrame = (FrameLayout) findViewById(R.id.main_content); + mDrawables = new Drawable[DRAW_IDS.length]; + for (int i = 0; i < DRAW_IDS.length; i++) { + mDrawables[i] = getResources().getDrawable(DRAW_IDS[i]); + } + init(); + if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(getIntent().getAction()) + || MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())) { + mCurrentModule = new VideoModule(); + mCurrentModuleIndex = VIDEO_MODULE_INDEX; + } else { + mCurrentModule = new PhotoModule(); + mCurrentModuleIndex = PHOTO_MODULE_INDEX; + } + mCurrentModule.init(this, mFrame, true); + mSwitcher.setCurrentIndex(mCurrentModuleIndex); + mOrientationListener = new MyOrientationEventListener(this); + } + + public void init() { + mControlsBackground = findViewById(R.id.controls); + mShutterSwitcher = findViewById(R.id.camera_shutter_switcher); + mShutter = (ShutterButton) findViewById(R.id.shutter_button); + mSwitcher = (CameraSwitcher) findViewById(R.id.camera_switcher); + int totaldrawid = (LightCycleHelper.hasLightCycleCapture(this) + ? DRAW_IDS.length : DRAW_IDS.length - 1); + if (!ApiHelper.HAS_OLD_PANORAMA) totaldrawid--; + + int[] drawids = new int[totaldrawid]; + int[] moduleids = new int[totaldrawid]; + int ix = 0; + for (int i = 0; i < mDrawables.length; i++) { + if (i == PANORAMA_MODULE_INDEX && !ApiHelper.HAS_OLD_PANORAMA) { + continue; // not enabled, so don't add to UI + } + if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(this)) { + continue; // not enabled, so don't add to UI + } + moduleids[ix] = i; + drawids[ix++] = DRAW_IDS[i]; + } + mSwitcher.setIds(moduleids, drawids); + mSwitcher.setSwitchListener(this); + mSwitcher.setCurrentIndex(mCurrentModuleIndex); + } + + private class MyOrientationEventListener + extends OrientationEventListener { + public MyOrientationEventListener(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; + mLastRawOrientation = orientation; + mCurrentModule.onOrientationChanged(orientation); + } + } + + @Override + public void onCameraSelected(int i) { + if (mPaused) return; + if (i != mCurrentModuleIndex) { + mPaused = true; + boolean canReuse = canReuseScreenNail(); + CameraHolder.instance().keep(); + closeModule(mCurrentModule); + mCurrentModuleIndex = i; + switch (i) { + case VIDEO_MODULE_INDEX: + mCurrentModule = new VideoModule(); + break; + case PHOTO_MODULE_INDEX: + mCurrentModule = new PhotoModule(); + break; + case PANORAMA_MODULE_INDEX: + mCurrentModule = new PanoramaModule(); + break; + case LIGHTCYCLE_MODULE_INDEX: + mCurrentModule = LightCycleHelper.createPanoramaModule(); + break; + } + openModule(mCurrentModule, canReuse); + mCurrentModule.onOrientationChanged(mLastRawOrientation); + } + } + + @Override + public void onShowSwitcherPopup() { + mCurrentModule.onShowSwitcherPopup(); + } + + private void openModule(CameraModule module, boolean canReuse) { + module.init(this, mFrame, canReuse && canReuseScreenNail()); + mPaused = false; + module.onResumeBeforeSuper(); + module.onResumeAfterSuper(); + } + + private void closeModule(CameraModule module) { + module.onPauseBeforeSuper(); + module.onPauseAfterSuper(); + mFrame.removeAllViews(); + } + + public ShutterButton getShutterButton() { + return mShutter; + } + + public void hideUI() { + mControlsBackground.setVisibility(View.INVISIBLE); + hideSwitcher(); + mShutter.setVisibility(View.GONE); + } + + public void showUI() { + mControlsBackground.setVisibility(View.VISIBLE); + showSwitcher(); + mShutter.setVisibility(View.VISIBLE); + // Force a layout change to show shutter button + mShutter.requestLayout(); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void showSwitcher() { + if (mCurrentModule.needsSwitcher()) { + mSwitcher.setVisibility(View.VISIBLE); + } + } + + public boolean isInCameraApp() { + return mShowCameraAppView; + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + + ViewGroup appRoot = (ViewGroup) findViewById(R.id.content); + // remove old switcher, shutter and shutter icon + View cameraControlsView = findViewById(R.id.camera_shutter_switcher); + appRoot.removeView(cameraControlsView); + + // create new layout with the current orientation + LayoutInflater inflater = getLayoutInflater(); + inflater.inflate(R.layout.camera_shutter_switcher, appRoot); + init(); + + if (mShowCameraAppView) { + showUI(); + } else { + hideUI(); + } + mCurrentModule.onConfigurationChanged(config); + } + + @Override + public void onPause() { + mPaused = true; + mOrientationListener.disable(); + mCurrentModule.onPauseBeforeSuper(); + super.onPause(); + mCurrentModule.onPauseAfterSuper(); + } + + @Override + public void onResume() { + mPaused = false; + mOrientationListener.enable(); + mCurrentModule.onResumeBeforeSuper(); + super.onResume(); + mCurrentModule.onResumeAfterSuper(); + } + + @Override + protected void onFullScreenChanged(boolean full) { + if (full) { + showUI(); + } else { + hideUI(); + } + super.onFullScreenChanged(full); + mCurrentModule.onFullScreenChanged(full); + } + + @Override + protected void onStop() { + super.onStop(); + mCurrentModule.onStop(); + getStateManager().clearTasks(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + getStateManager().clearActivityResult(); + } + + @Override + protected void installIntentFilter() { + super.installIntentFilter(); + mCurrentModule.installIntentFilter(); + } + + @Override + protected void onActivityResult( + int requestCode, int resultCode, Intent data) { + // Only PhotoPage understands ProxyLauncher.RESULT_USER_CANCELED + if (resultCode == ProxyLauncher.RESULT_USER_CANCELED + && !(getStateManager().getTopState() instanceof PhotoPage)) { + resultCode = RESULT_CANCELED; + } + super.onActivityResult(requestCode, resultCode, data); + // Unmap cancel vs. reset + if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) { + resultCode = RESULT_CANCELED; + } + mCurrentModule.onActivityResult(requestCode, resultCode, data); + } + + // Preview area is touched. Handle touch focus. + @Override + protected void onSingleTapUp(View view, int x, int y) { + mCurrentModule.onSingleTapUp(view, x, y); + } + + @Override + public void onBackPressed() { + if (!mCurrentModule.onBackPressed()) { + super.onBackPressed(); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mCurrentModule.onKeyDown(keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mCurrentModule.onKeyUp(keyCode, event) + || super.onKeyUp(keyCode, event); + } + + public void cancelActivityTouchHandling() { + if (mDown != null) { + MotionEvent cancel = MotionEvent.obtain(mDown); + cancel.setAction(MotionEvent.ACTION_CANCEL); + super.dispatchTouchEvent(cancel); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (m.getActionMasked() == MotionEvent.ACTION_DOWN) { + mDown = m; + } + if ((mSwitcher != null) && mSwitcher.showsPopup() && !mSwitcher.isInsidePopup(m)) { + return mSwitcher.onTouch(null, m); + } else { + return mShutterSwitcher.dispatchTouchEvent(m) + || mCurrentModule.dispatchTouchEvent(m); + } + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + Intent proxyIntent = new Intent(this, ProxyLauncher.class); + proxyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + proxyIntent.putExtra(Intent.EXTRA_INTENT, intent); + super.startActivityForResult(proxyIntent, requestCode); + } + + public boolean superDispatchTouchEvent(MotionEvent m) { + return super.dispatchTouchEvent(m); + } + + // Preview texture has been copied. Now camera can be released and the + // animation can be started. + @Override + public void onPreviewTextureCopied() { + mCurrentModule.onPreviewTextureCopied(); + } + + @Override + public void onCaptureTextureCopied() { + mCurrentModule.onCaptureTextureCopied(); + } + + @Override + public void onUserInteraction() { + super.onUserInteraction(); + mCurrentModule.onUserInteraction(); + } + + @Override + protected boolean updateStorageHintOnResume() { + return mCurrentModule.updateStorageHintOnResume(); + } + + @Override + public void updateCameraAppView() { + super.updateCameraAppView(); + mCurrentModule.updateCameraAppView(); + } + + private boolean canReuseScreenNail() { + return mCurrentModuleIndex == PHOTO_MODULE_INDEX + || mCurrentModuleIndex == VIDEO_MODULE_INDEX + || mCurrentModuleIndex == LIGHTCYCLE_MODULE_INDEX; + } + + @Override + public boolean isPanoramaActivity() { + return (mCurrentModuleIndex == PANORAMA_MODULE_INDEX); + } + + // Accessor methods for getting latency times used in performance testing + public long getAutoFocusTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mAutoFocusTime : -1; + } + + public long getShutterLag() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mShutterLag : -1; + } + + public long getShutterToPictureDisplayedTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1; + } + + public long getPictureDisplayedToJpegCallbackTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1; + } + + public long getJpegCallbackFinishTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1; + } + + public long getCaptureStartTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mCaptureStartTime : -1; + } + + public boolean isRecording() { + return (mCurrentModule instanceof VideoModule) ? + ((VideoModule) mCurrentModule).isRecording() : false; + } + + public CameraScreenNail getCameraScreenNail() { + return (CameraScreenNail) mCameraScreenNail; + } +} diff --git a/src/com/android/camera/CameraBackupAgent.java b/src/com/android/camera/CameraBackupAgent.java new file mode 100644 index 000000000..30ba212df --- /dev/null +++ b/src/com/android/camera/CameraBackupAgent.java @@ -0,0 +1,32 @@ +/* + * 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; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.SharedPreferencesBackupHelper; +import android.content.Context; + +public class CameraBackupAgent extends BackupAgentHelper { + private static final String CAMERA_BACKUP_KEY = "camera_prefs"; + + public void onCreate () { + Context context = getApplicationContext(); + String prefNames[] = ComboPreferences.getSharedPreferencesNames(context); + + addHelper(CAMERA_BACKUP_KEY, new SharedPreferencesBackupHelper(context, prefNames)); + } +} diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java new file mode 100644 index 000000000..a65942d57 --- /dev/null +++ b/src/com/android/camera/CameraButtonIntentReceiver.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2007 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * {@code CameraButtonIntentReceiver} is invoked when the camera button is + * long-pressed. + * + * It is declared in {@code AndroidManifest.xml} to receive the + * {@code android.intent.action.CAMERA_BUTTON} intent. + * + * After making sure we can use the camera hardware, it starts the Camera + * activity. + */ +public class CameraButtonIntentReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // Try to get the camera hardware + CameraHolder holder = CameraHolder.instance(); + ComboPreferences pref = new ComboPreferences(context); + int cameraId = CameraSettings.readPreferredCameraId(pref); + if (holder.tryOpen(cameraId) == null) return; + + // We are going to launch the camera, so hold the camera for later use + holder.keep(); + holder.release(); + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClass(context, CameraActivity.class); + i.addCategory(Intent.CATEGORY_LAUNCHER); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(i); + } +} diff --git a/src/com/android/camera/CameraDisabledException.java b/src/com/android/camera/CameraDisabledException.java new file mode 100644 index 000000000..512809be6 --- /dev/null +++ b/src/com/android/camera/CameraDisabledException.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * This class represents the condition that device policy manager has disabled + * the camera. + */ +public class CameraDisabledException extends Exception { +} diff --git a/src/com/android/camera/CameraErrorCallback.java b/src/com/android/camera/CameraErrorCallback.java new file mode 100644 index 000000000..22f800ef9 --- /dev/null +++ b/src/com/android/camera/CameraErrorCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.util.Log; + +public class CameraErrorCallback + implements android.hardware.Camera.ErrorCallback { + private static final String TAG = "CameraErrorCallback"; + + @Override + public void onError(int error, android.hardware.Camera camera) { + Log.e(TAG, "Got camera error callback. error=" + error); + if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) { + // We are not sure about the current state of the app (in preview or + // snapshot or recording). Closing the app is better than creating a + // new Camera object. + throw new RuntimeException("Media server died."); + } + } +} diff --git a/src/com/android/camera/CameraHardwareException.java b/src/com/android/camera/CameraHardwareException.java new file mode 100644 index 000000000..82090554d --- /dev/null +++ b/src/com/android/camera/CameraHardwareException.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * This class represents the condition that we cannot open the camera hardware + * successfully. For example, another process is using the camera. + */ +public class CameraHardwareException extends Exception { + + public CameraHardwareException(Throwable t) { + super(t); + } +} diff --git a/src/com/android/camera/CameraHolder.java b/src/com/android/camera/CameraHolder.java new file mode 100644 index 000000000..5b7bbfda3 --- /dev/null +++ b/src/com/android/camera/CameraHolder.java @@ -0,0 +1,298 @@ +/* + * 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 static com.android.camera.Util.Assert; + +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.camera.CameraManager.CameraProxy; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +/** + * The class is used to hold an {@code android.hardware.Camera} instance. + * + *

The {@code open()} and {@code release()} calls are similar to the ones + * in {@code android.hardware.Camera}. The difference is if {@code keep()} is + * called before {@code release()}, CameraHolder will try to hold the {@code + * android.hardware.Camera} instance for a while, so if {@code open()} is + * called soon after, we can avoid the cost of {@code open()} in {@code + * android.hardware.Camera}. + * + *

This is used in switching between different modules. + */ +public class CameraHolder { + private static final String TAG = "CameraHolder"; + private static final int KEEP_CAMERA_TIMEOUT = 3000; // 3 seconds + private CameraProxy mCameraDevice; + private long mKeepBeforeTime; // Keep the Camera before this time. + private final Handler mHandler; + private boolean mCameraOpened; // true if camera is opened + private final int mNumberOfCameras; + private int mCameraId = -1; // current camera id + private int mBackCameraId = -1; + private int mFrontCameraId = -1; + private final CameraInfo[] mInfo; + private static CameraProxy mMockCamera[]; + private static CameraInfo mMockCameraInfo[]; + + /* Debug double-open issue */ + private static final boolean DEBUG_OPEN_RELEASE = true; + private static class OpenReleaseState { + long time; + int id; + String device; + String[] stack; + } + private static ArrayList sOpenReleaseStates = + new ArrayList(); + private static SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS"); + + private static synchronized void collectState(int id, CameraProxy device) { + OpenReleaseState s = new OpenReleaseState(); + s.time = System.currentTimeMillis(); + s.id = id; + if (device == null) { + s.device = "(null)"; + } else { + s.device = device.toString(); + } + + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + String[] lines = new String[stack.length]; + for (int i = 0; i < stack.length; i++) { + lines[i] = stack[i].toString(); + } + s.stack = lines; + + if (sOpenReleaseStates.size() > 10) { + sOpenReleaseStates.remove(0); + } + sOpenReleaseStates.add(s); + } + + private static synchronized void dumpStates() { + for (int i = sOpenReleaseStates.size() - 1; i >= 0; i--) { + OpenReleaseState s = sOpenReleaseStates.get(i); + String date = sDateFormat.format(new Date(s.time)); + Log.d(TAG, "State " + i + " at " + date); + Log.d(TAG, "mCameraId = " + s.id + ", mCameraDevice = " + s.device); + Log.d(TAG, "Stack:"); + for (int j = 0; j < s.stack.length; j++) { + Log.d(TAG, " " + s.stack[j]); + } + } + } + + // We store the camera parameters when we actually open the device, + // so we can restore them in the subsequent open() requests by the user. + // This prevents the parameters set by PhotoModule used by VideoModule + // inadvertently. + private Parameters mParameters; + + // Use a singleton. + private static CameraHolder sHolder; + public static synchronized CameraHolder instance() { + if (sHolder == null) { + sHolder = new CameraHolder(); + } + return sHolder; + } + + private static final int RELEASE_CAMERA = 1; + private class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case RELEASE_CAMERA: + synchronized (CameraHolder.this) { + // In 'CameraHolder.open', the 'RELEASE_CAMERA' message + // will be removed if it is found in the queue. However, + // there is a chance that this message has been handled + // before being removed. So, we need to add a check + // here: + if (!mCameraOpened) release(); + } + break; + } + } + } + + public static void injectMockCamera(CameraInfo[] info, CameraProxy[] camera) { + mMockCameraInfo = info; + mMockCamera = camera; + sHolder = new CameraHolder(); + } + + private CameraHolder() { + HandlerThread ht = new HandlerThread("CameraHolder"); + ht.start(); + mHandler = new MyHandler(ht.getLooper()); + if (mMockCameraInfo != null) { + mNumberOfCameras = mMockCameraInfo.length; + mInfo = mMockCameraInfo; + } else { + mNumberOfCameras = android.hardware.Camera.getNumberOfCameras(); + mInfo = new CameraInfo[mNumberOfCameras]; + for (int i = 0; i < mNumberOfCameras; i++) { + mInfo[i] = new CameraInfo(); + android.hardware.Camera.getCameraInfo(i, mInfo[i]); + } + } + + // get the first (smallest) back and first front camera id + for (int i = 0; i < mNumberOfCameras; i++) { + if (mBackCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) { + mBackCameraId = i; + } else if (mFrontCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) { + mFrontCameraId = i; + } + } + } + + public int getNumberOfCameras() { + return mNumberOfCameras; + } + + public CameraInfo[] getCameraInfo() { + return mInfo; + } + + public synchronized CameraProxy open(int cameraId) + throws CameraHardwareException { + if (DEBUG_OPEN_RELEASE) { + collectState(cameraId, mCameraDevice); + if (mCameraOpened) { + Log.e(TAG, "double open"); + dumpStates(); + } + } + Assert(!mCameraOpened); + if (mCameraDevice != null && mCameraId != cameraId) { + mCameraDevice.release(); + mCameraDevice = null; + mCameraId = -1; + } + if (mCameraDevice == null) { + try { + Log.v(TAG, "open camera " + cameraId); + if (mMockCameraInfo == null) { + mCameraDevice = CameraManager.instance().cameraOpen(cameraId); + } else { + if (mMockCamera == null) + throw new RuntimeException(); + mCameraDevice = mMockCamera[cameraId]; + } + mCameraId = cameraId; + } catch (RuntimeException e) { + Log.e(TAG, "fail to connect Camera", e); + throw new CameraHardwareException(e); + } + mParameters = mCameraDevice.getParameters(); + } else { + try { + mCameraDevice.reconnect(); + } catch (IOException e) { + Log.e(TAG, "reconnect failed."); + throw new CameraHardwareException(e); + } + mCameraDevice.setParameters(mParameters); + } + mCameraOpened = true; + mHandler.removeMessages(RELEASE_CAMERA); + mKeepBeforeTime = 0; + return mCameraDevice; + } + + /** + * Tries to open the hardware camera. If the camera is being used or + * unavailable then return {@code null}. + */ + public synchronized CameraProxy tryOpen(int cameraId) { + try { + return !mCameraOpened ? open(cameraId) : null; + } catch (CameraHardwareException e) { + // In eng build, we throw the exception so that test tool + // can detect it and report it + if ("eng".equals(Build.TYPE)) { + throw new RuntimeException(e); + } + return null; + } + } + + public synchronized void release() { + if (DEBUG_OPEN_RELEASE) { + collectState(mCameraId, mCameraDevice); + } + + if (mCameraDevice == null) return; + + long now = System.currentTimeMillis(); + if (now < mKeepBeforeTime) { + if (mCameraOpened) { + mCameraOpened = false; + mCameraDevice.stopPreview(); + } + mHandler.sendEmptyMessageDelayed(RELEASE_CAMERA, + mKeepBeforeTime - now); + return; + } + mCameraOpened = false; + mCameraDevice.release(); + mCameraDevice = null; + // We must set this to null because it has a reference to Camera. + // Camera has references to the listeners. + mParameters = null; + mCameraId = -1; + } + + public void keep() { + keep(KEEP_CAMERA_TIMEOUT); + } + + public synchronized void keep(int time) { + // We allow mCameraOpened in either state for the convenience of the + // calling activity. The activity may not have a chance to call open() + // before the user switches to another activity. + mKeepBeforeTime = System.currentTimeMillis() + time; + } + + public int getBackCameraId() { + return mBackCameraId; + } + + public int getFrontCameraId() { + return mFrontCameraId; + } +} diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java new file mode 100644 index 000000000..854e1058f --- /dev/null +++ b/src/com/android/camera/CameraManager.java @@ -0,0 +1,490 @@ +/* + * 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; + +import static com.android.camera.Util.Assert; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.AutoFocusCallback; +import android.hardware.Camera.AutoFocusMoveCallback; +import android.hardware.Camera.ErrorCallback; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.OnZoomChangeListener; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.ShutterCallback; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.view.SurfaceHolder; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.IOException; + +public class CameraManager { + private static final String TAG = "CameraManager"; + private static CameraManager sCameraManager = new CameraManager(); + + // Thread progress signals + private ConditionVariable mSig = new ConditionVariable(); + + private Parameters mParameters; + private IOException mReconnectException; + + private static final int RELEASE = 1; + private static final int RECONNECT = 2; + private static final int UNLOCK = 3; + private static final int LOCK = 4; + private static final int SET_PREVIEW_TEXTURE_ASYNC = 5; + private static final int START_PREVIEW_ASYNC = 6; + private static final int STOP_PREVIEW = 7; + private static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 8; + private static final int ADD_CALLBACK_BUFFER = 9; + private static final int AUTO_FOCUS = 10; + private static final int CANCEL_AUTO_FOCUS = 11; + private static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 12; + private static final int SET_DISPLAY_ORIENTATION = 13; + private static final int SET_ZOOM_CHANGE_LISTENER = 14; + private static final int SET_FACE_DETECTION_LISTENER = 15; + private static final int START_FACE_DETECTION = 16; + private static final int STOP_FACE_DETECTION = 17; + private static final int SET_ERROR_CALLBACK = 18; + private static final int SET_PARAMETERS = 19; + private static final int GET_PARAMETERS = 20; + private static final int SET_PARAMETERS_ASYNC = 21; + private static final int WAIT_FOR_IDLE = 22; + private static final int SET_PREVIEW_DISPLAY_ASYNC = 23; + private static final int SET_PREVIEW_CALLBACK = 24; + private static final int ENABLE_SHUTTER_SOUND = 25; + + private Handler mCameraHandler; + private CameraProxy mCameraProxy; + private android.hardware.Camera mCamera; + + public static CameraManager instance() { + return sCameraManager; + } + + private CameraManager() { + HandlerThread ht = new HandlerThread("Camera Handler Thread"); + ht.start(); + mCameraHandler = new CameraHandler(ht.getLooper()); + } + + private class CameraHandler extends Handler { + CameraHandler(Looper looper) { + super(looper); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void startFaceDetection() { + mCamera.startFaceDetection(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void stopFaceDetection() { + mCamera.stopFaceDetection(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setFaceDetectionListener(FaceDetectionListener listener) { + mCamera.setFaceDetectionListener(listener); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private void setPreviewTexture(Object surfaceTexture) { + try { + mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN_MR1) + private void enableShutterSound(boolean enable) { + mCamera.enableShutterSound(enable); + } + + /* + * This method does not deal with the build version check. Everyone should + * check first before sending message to this handler. + */ + @Override + public void handleMessage(final Message msg) { + try { + switch (msg.what) { + case RELEASE: + mCamera.release(); + mCamera = null; + mCameraProxy = null; + break; + + case RECONNECT: + mReconnectException = null; + try { + mCamera.reconnect(); + } catch (IOException ex) { + mReconnectException = ex; + } + break; + + case UNLOCK: + mCamera.unlock(); + break; + + case LOCK: + mCamera.lock(); + break; + + case SET_PREVIEW_TEXTURE_ASYNC: + setPreviewTexture(msg.obj); + return; // no need to call mSig.open() + + case SET_PREVIEW_DISPLAY_ASYNC: + try { + mCamera.setPreviewDisplay((SurfaceHolder) msg.obj); + } catch(IOException e) { + throw new RuntimeException(e); + } + return; // no need to call mSig.open() + + case START_PREVIEW_ASYNC: + mCamera.startPreview(); + return; // no need to call mSig.open() + + case STOP_PREVIEW: + mCamera.stopPreview(); + break; + + case SET_PREVIEW_CALLBACK_WITH_BUFFER: + mCamera.setPreviewCallbackWithBuffer( + (PreviewCallback) msg.obj); + break; + + case ADD_CALLBACK_BUFFER: + mCamera.addCallbackBuffer((byte[]) msg.obj); + break; + + case AUTO_FOCUS: + mCamera.autoFocus((AutoFocusCallback) msg.obj); + break; + + case CANCEL_AUTO_FOCUS: + mCamera.cancelAutoFocus(); + break; + + case SET_AUTO_FOCUS_MOVE_CALLBACK: + setAutoFocusMoveCallback(mCamera, msg.obj); + break; + + case SET_DISPLAY_ORIENTATION: + mCamera.setDisplayOrientation(msg.arg1); + break; + + case SET_ZOOM_CHANGE_LISTENER: + mCamera.setZoomChangeListener( + (OnZoomChangeListener) msg.obj); + break; + + case SET_FACE_DETECTION_LISTENER: + setFaceDetectionListener((FaceDetectionListener) msg.obj); + break; + + case START_FACE_DETECTION: + startFaceDetection(); + break; + + case STOP_FACE_DETECTION: + stopFaceDetection(); + break; + + case SET_ERROR_CALLBACK: + mCamera.setErrorCallback((ErrorCallback) msg.obj); + break; + + case SET_PARAMETERS: + mCamera.setParameters((Parameters) msg.obj); + break; + + case GET_PARAMETERS: + mParameters = mCamera.getParameters(); + break; + + case SET_PARAMETERS_ASYNC: + mCamera.setParameters((Parameters) msg.obj); + return; // no need to call mSig.open() + + case SET_PREVIEW_CALLBACK: + mCamera.setPreviewCallback((PreviewCallback) msg.obj); + break; + + case ENABLE_SHUTTER_SOUND: + enableShutterSound((msg.arg1 == 1) ? true : false); + break; + + case WAIT_FOR_IDLE: + // do nothing + break; + + default: + throw new RuntimeException("Invalid CameraProxy message=" + msg.what); + } + } catch (RuntimeException e) { + if (msg.what != RELEASE && mCamera != null) { + try { + mCamera.release(); + } catch (Exception ex) { + Log.e(TAG, "Fail to release the camera."); + } + mCamera = null; + mCameraProxy = null; + } + throw e; + } + mSig.open(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoFocusMoveCallback(android.hardware.Camera camera, + Object cb) { + camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb); + } + + // Open camera synchronously. This method is invoked in the context of a + // background thread. + CameraProxy cameraOpen(int cameraId) { + // Cannot open camera in mCameraHandler, otherwise all camera events + // will be routed to mCameraHandler looper, which in turn will call + // event handler like Camera.onFaceDetection, which in turn will modify + // UI and cause exception like this: + // CalledFromWrongThreadException: Only the original thread that created + // a view hierarchy can touch its views. + mCamera = android.hardware.Camera.open(cameraId); + if (mCamera != null) { + mCameraProxy = new CameraProxy(); + return mCameraProxy; + } else { + return null; + } + } + + public class CameraProxy { + private CameraProxy() { + Assert(mCamera != null); + } + + public android.hardware.Camera getCamera() { + return mCamera; + } + + public void release() { + mSig.close(); + mCameraHandler.sendEmptyMessage(RELEASE); + mSig.block(); + } + + public void reconnect() throws IOException { + mSig.close(); + mCameraHandler.sendEmptyMessage(RECONNECT); + mSig.block(); + if (mReconnectException != null) { + throw mReconnectException; + } + } + + public void unlock() { + mSig.close(); + mCameraHandler.sendEmptyMessage(UNLOCK); + mSig.block(); + } + + public void lock() { + mSig.close(); + mCameraHandler.sendEmptyMessage(LOCK); + mSig.block(); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + public void setPreviewTextureAsync(final SurfaceTexture surfaceTexture) { + mCameraHandler.obtainMessage(SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture).sendToTarget(); + } + + public void setPreviewDisplayAsync(final SurfaceHolder surfaceHolder) { + mCameraHandler.obtainMessage(SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder).sendToTarget(); + } + + public void startPreviewAsync() { + mCameraHandler.sendEmptyMessage(START_PREVIEW_ASYNC); + } + + public void stopPreview() { + mSig.close(); + mCameraHandler.sendEmptyMessage(STOP_PREVIEW); + mSig.block(); + } + + public void setPreviewCallback(final PreviewCallback cb) { + mSig.close(); + mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK, cb).sendToTarget(); + mSig.block(); + } + + public void setPreviewCallbackWithBuffer(final PreviewCallback cb) { + mSig.close(); + mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK_WITH_BUFFER, cb).sendToTarget(); + mSig.block(); + } + + public void addCallbackBuffer(byte[] callbackBuffer) { + mSig.close(); + mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget(); + mSig.block(); + } + + public void autoFocus(AutoFocusCallback cb) { + mSig.close(); + mCameraHandler.obtainMessage(AUTO_FOCUS, cb).sendToTarget(); + mSig.block(); + } + + public void cancelAutoFocus() { + mSig.close(); + mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS); + mSig.block(); + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) { + mSig.close(); + mCameraHandler.obtainMessage(SET_AUTO_FOCUS_MOVE_CALLBACK, cb).sendToTarget(); + mSig.block(); + } + + public void takePicture(final ShutterCallback shutter, final PictureCallback raw, + final PictureCallback postview, final PictureCallback jpeg) { + mSig.close(); + // Too many parameters, so use post for simplicity + mCameraHandler.post(new Runnable() { + @Override + public void run() { + mCamera.takePicture(shutter, raw, postview, jpeg); + mSig.open(); + } + }); + mSig.block(); + } + + public void takePicture2(final ShutterCallback shutter, final PictureCallback raw, + final PictureCallback postview, final PictureCallback jpeg, + final int cameraState, final int focusState) { + mSig.close(); + // Too many parameters, so use post for simplicity + mCameraHandler.post(new Runnable() { + @Override + public void run() { + try { + mCamera.takePicture(shutter, raw, postview, jpeg); + } catch (RuntimeException e) { + Log.w(TAG, "take picture failed; cameraState:" + cameraState + + ", focusState:" + focusState); + throw e; + } + mSig.open(); + } + }); + mSig.block(); + } + + public void setDisplayOrientation(int degrees) { + mSig.close(); + mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0) + .sendToTarget(); + mSig.block(); + } + + public void setZoomChangeListener(OnZoomChangeListener listener) { + mSig.close(); + mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget(); + mSig.block(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public void setFaceDetectionListener(FaceDetectionListener listener) { + mSig.close(); + mCameraHandler.obtainMessage(SET_FACE_DETECTION_LISTENER, listener).sendToTarget(); + mSig.block(); + } + + public void startFaceDetection() { + mSig.close(); + mCameraHandler.sendEmptyMessage(START_FACE_DETECTION); + mSig.block(); + } + + public void stopFaceDetection() { + mSig.close(); + mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION); + mSig.block(); + } + + public void setErrorCallback(ErrorCallback cb) { + mSig.close(); + mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget(); + mSig.block(); + } + + public void setParameters(Parameters params) { + mSig.close(); + mCameraHandler.obtainMessage(SET_PARAMETERS, params).sendToTarget(); + mSig.block(); + } + + public void setParametersAsync(Parameters params) { + mCameraHandler.removeMessages(SET_PARAMETERS_ASYNC); + mCameraHandler.obtainMessage(SET_PARAMETERS_ASYNC, params).sendToTarget(); + } + + public Parameters getParameters() { + mSig.close(); + mCameraHandler.sendEmptyMessage(GET_PARAMETERS); + mSig.block(); + Parameters parameters = mParameters; + mParameters = null; + return parameters; + } + + public void enableShutterSound(boolean enable) { + mSig.close(); + mCameraHandler.obtainMessage( + ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget(); + mSig.block(); + } + + public void waitForIdle() { + mSig.close(); + mCameraHandler.sendEmptyMessage(WAIT_FOR_IDLE); + mSig.block(); + } + } +} diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java new file mode 100644 index 000000000..8e022d665 --- /dev/null +++ b/src/com/android/camera/CameraModule.java @@ -0,0 +1,75 @@ +/* + * 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; + +import android.content.Intent; +import android.content.res.Configuration; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public interface CameraModule { + + public void init(CameraActivity activity, View frame, boolean reuseScreenNail); + + public void onFullScreenChanged(boolean full); + + public void onPauseBeforeSuper(); + + public void onPauseAfterSuper(); + + public void onResumeBeforeSuper(); + + public void onResumeAfterSuper(); + + public void onConfigurationChanged(Configuration config); + + public void onStop(); + + public void installIntentFilter(); + + public void onActivityResult(int requestCode, int resultCode, Intent data); + + public boolean onBackPressed(); + + public boolean onKeyDown(int keyCode, KeyEvent event); + + public boolean onKeyUp(int keyCode, KeyEvent event); + + public void onSingleTapUp(View view, int x, int y); + + public boolean dispatchTouchEvent(MotionEvent m); + + public void onPreviewTextureCopied(); + + public void onCaptureTextureCopied(); + + public void onUserInteraction(); + + public boolean updateStorageHintOnResume(); + + public void updateCameraAppView(); + + public boolean collapseCameraControls(); + + public boolean needsSwitcher(); + + public void onOrientationChanged(int orientation); + + public void onShowSwitcherPopup(); + +} diff --git a/src/com/android/camera/CameraPreference.java b/src/com/android/camera/CameraPreference.java new file mode 100644 index 000000000..0a4e9b3ce --- /dev/null +++ b/src/com/android/camera/CameraPreference.java @@ -0,0 +1,61 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +/** + * The base class of all Preferences used in Camera. The preferences can be + * loaded from XML resource by PreferenceInflater. + */ +public abstract class CameraPreference { + + private final String mTitle; + private SharedPreferences mSharedPreferences; + private final Context mContext; + + static public interface OnPreferenceChangedListener { + public void onSharedPreferenceChanged(); + public void onRestorePreferencesClicked(); + public void onOverriddenPreferencesClicked(); + public void onCameraPickerClicked(int cameraId); + } + + public CameraPreference(Context context, AttributeSet attrs) { + mContext = context; + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.CameraPreference, 0, 0); + mTitle = a.getString(R.styleable.CameraPreference_title); + a.recycle(); + } + + public String getTitle() { + return mTitle; + } + + public SharedPreferences getSharedPreferences() { + if (mSharedPreferences == null) { + mSharedPreferences = ComboPreferences.get(mContext); + } + return mSharedPreferences; + } + + public abstract void reloadValue(); +} diff --git a/src/com/android/camera/CameraScreenNail.java b/src/com/android/camera/CameraScreenNail.java new file mode 100644 index 000000000..5d3c5c092 --- /dev/null +++ b/src/com/android/camera/CameraScreenNail.java @@ -0,0 +1,497 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.opengl.Matrix; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.SurfaceTextureScreenNail; + +/* + * This is a ScreenNail which can display camera's preview. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) +public class CameraScreenNail extends SurfaceTextureScreenNail { + private static final String TAG = "CAM_ScreenNail"; + private static final int ANIM_NONE = 0; + // Capture animation is about to start. + private static final int ANIM_CAPTURE_START = 1; + // Capture animation is running. + private static final int ANIM_CAPTURE_RUNNING = 2; + // Switch camera animation needs to copy texture. + private static final int ANIM_SWITCH_COPY_TEXTURE = 3; + // Switch camera animation shows the initial feedback by darkening the + // preview. + private static final int ANIM_SWITCH_DARK_PREVIEW = 4; + // Switch camera animation is waiting for the first frame. + private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5; + // Switch camera animation is about to start. + private static final int ANIM_SWITCH_START = 6; + // Switch camera animation is running. + private static final int ANIM_SWITCH_RUNNING = 7; + + private boolean mVisible; + // True if first onFrameAvailable has been called. If screen nail is drawn + // too early, it will be all white. + private boolean mFirstFrameArrived; + private Listener mListener; + private final float[] mTextureTransformMatrix = new float[16]; + + // Animation. + private CaptureAnimManager mCaptureAnimManager = new CaptureAnimManager(); + private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager(); + private int mAnimState = ANIM_NONE; + private RawTexture mAnimTexture; + // Some methods are called by GL thread and some are called by main thread. + // This protects mAnimState, mVisible, and surface texture. This also makes + // sure some code are atomic. For example, requestRender and setting + // mAnimState. + private Object mLock = new Object(); + + private OnFrameDrawnListener mOneTimeFrameDrawnListener; + private int mRenderWidth; + private int mRenderHeight; + // This represents the scaled, uncropped size of the texture + // Needed for FaceView + private int mUncroppedRenderWidth; + private int mUncroppedRenderHeight; + private float mScaleX = 1f, mScaleY = 1f; + private boolean mFullScreen; + private boolean mEnableAspectRatioClamping = false; + private boolean mAcquireTexture = false; + private final DrawClient mDefaultDraw = new DrawClient() { + @Override + public void onDraw(GLCanvas canvas, int x, int y, int width, int height) { + CameraScreenNail.super.draw(canvas, x, y, width, height); + } + + @Override + public boolean requiresSurfaceTexture() { + return true; + } + }; + private DrawClient mDraw = mDefaultDraw; + + public interface Listener { + void requestRender(); + // Preview has been copied to a texture. + void onPreviewTextureCopied(); + + void onCaptureTextureCopied(); + } + + public interface OnFrameDrawnListener { + void onFrameDrawn(CameraScreenNail c); + } + + public interface DrawClient { + void onDraw(GLCanvas canvas, int x, int y, int width, int height); + + boolean requiresSurfaceTexture(); + } + + public CameraScreenNail(Listener listener) { + mListener = listener; + } + + public void setFullScreen(boolean full) { + synchronized (mLock) { + mFullScreen = full; + } + } + + /** + * returns the uncropped, but scaled, width of the rendered texture + */ + public int getUncroppedRenderWidth() { + return mUncroppedRenderWidth; + } + + /** + * returns the uncropped, but scaled, width of the rendered texture + */ + public int getUncroppedRenderHeight() { + return mUncroppedRenderHeight; + } + + @Override + public int getWidth() { + return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth(); + } + + @Override + public int getHeight() { + return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight(); + } + + private int getTextureWidth() { + return super.getWidth(); + } + + private int getTextureHeight() { + return super.getHeight(); + } + + @Override + public void setSize(int w, int h) { + super.setSize(w, h); + mEnableAspectRatioClamping = false; + if (mRenderWidth == 0) { + mRenderWidth = w; + mRenderHeight = h; + } + updateRenderSize(); + } + + /** + * Tells the ScreenNail to override the default aspect ratio scaling + * and instead perform custom scaling to basically do a centerCrop instead + * of the default centerInside + * + * Note that calls to setSize will disable this + */ + public void enableAspectRatioClamping() { + mEnableAspectRatioClamping = true; + updateRenderSize(); + } + + private void setPreviewLayoutSize(int w, int h) { + Log.i(TAG, "preview layout size: "+w+"/"+h); + mRenderWidth = w; + mRenderHeight = h; + updateRenderSize(); + } + + private void updateRenderSize() { + if (!mEnableAspectRatioClamping) { + mScaleX = mScaleY = 1f; + mUncroppedRenderWidth = getTextureWidth(); + mUncroppedRenderHeight = getTextureHeight(); + Log.i(TAG, "aspect ratio clamping disabled"); + return; + } + + float aspectRatio; + if (getTextureWidth() > getTextureHeight()) { + aspectRatio = (float) getTextureWidth() / (float) getTextureHeight(); + } else { + aspectRatio = (float) getTextureHeight() / (float) getTextureWidth(); + } + float scaledTextureWidth, scaledTextureHeight; + if (mRenderWidth > mRenderHeight) { + scaledTextureWidth = Math.max(mRenderWidth, + (int) (mRenderHeight * aspectRatio)); + scaledTextureHeight = Math.max(mRenderHeight, + (int)(mRenderWidth / aspectRatio)); + } else { + scaledTextureWidth = Math.max(mRenderWidth, + (int) (mRenderHeight / aspectRatio)); + scaledTextureHeight = Math.max(mRenderHeight, + (int) (mRenderWidth * aspectRatio)); + } + mScaleX = mRenderWidth / scaledTextureWidth; + mScaleY = mRenderHeight / scaledTextureHeight; + mUncroppedRenderWidth = Math.round(scaledTextureWidth); + mUncroppedRenderHeight = Math.round(scaledTextureHeight); + Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY); + } + + public void acquireSurfaceTexture() { + synchronized (mLock) { + mFirstFrameArrived = false; + mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true); + mAcquireTexture = true; + } + mListener.requestRender(); + } + + @Override + public void releaseSurfaceTexture() { + synchronized (mLock) { + if (mAcquireTexture) { + mAcquireTexture = false; + mLock.notifyAll(); + } else { + if (super.getSurfaceTexture() != null) { + super.releaseSurfaceTexture(); + } + mAnimState = ANIM_NONE; // stop the animation + } + } + } + + public void copyTexture() { + synchronized (mLock) { + mListener.requestRender(); + mAnimState = ANIM_SWITCH_COPY_TEXTURE; + } + } + + public void animateSwitchCamera() { + Log.v(TAG, "animateSwitchCamera"); + synchronized (mLock) { + if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) { + // Do not request render here because camera has been just + // started. We do not want to draw black frames. + mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME; + } + } + } + + public void animateCapture(int displayRotation) { + synchronized (mLock) { + mCaptureAnimManager.setOrientation(displayRotation); + mCaptureAnimManager.animateFlashAndSlide(); + mListener.requestRender(); + mAnimState = ANIM_CAPTURE_START; + } + } + + public RawTexture getAnimationTexture() { + return mAnimTexture; + } + + public void animateFlash(int displayRotation) { + synchronized (mLock) { + mCaptureAnimManager.setOrientation(displayRotation); + mCaptureAnimManager.animateFlash(); + mListener.requestRender(); + mAnimState = ANIM_CAPTURE_START; + } + } + + public void animateSlide() { + synchronized (mLock) { + // Ignore the case where animateFlash is skipped but animateSlide is called + // e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back + // to camera. This case only happens in monkey tests, not applicable to normal + // human beings. + if (mAnimState != ANIM_CAPTURE_RUNNING) { + Log.v(TAG, "Cannot animateSlide outside of animateCapture!" + + " Animation state = " + mAnimState); + return; + } + mCaptureAnimManager.animateSlide(); + mListener.requestRender(); + } + } + + private void callbackIfNeeded() { + if (mOneTimeFrameDrawnListener != null) { + mOneTimeFrameDrawnListener.onFrameDrawn(this); + mOneTimeFrameDrawnListener = null; + } + } + + @Override + protected void updateTransformMatrix(float[] matrix) { + super.updateTransformMatrix(matrix); + Matrix.translateM(matrix, 0, .5f, .5f, 0); + Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f); + Matrix.translateM(matrix, 0, -.5f, -.5f, 0); + } + + public void directDraw(GLCanvas canvas, int x, int y, int width, int height) { + DrawClient draw; + synchronized (mLock) { + draw = mDraw; + } + draw.onDraw(canvas, x, y, width, height); + } + + public void setDraw(DrawClient draw) { + synchronized (mLock) { + if (draw == null) { + mDraw = mDefaultDraw; + } else { + mDraw = draw; + } + } + mListener.requestRender(); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + synchronized (mLock) { + allocateTextureIfRequested(canvas); + if (!mVisible) mVisible = true; + SurfaceTexture surfaceTexture = getSurfaceTexture(); + if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) { + return; + } + + switch (mAnimState) { + case ANIM_NONE: + directDraw(canvas, x, y, width, height); + break; + case ANIM_SWITCH_COPY_TEXTURE: + copyPreviewTexture(canvas); + mSwitchAnimManager.setReviewDrawingSize(width, height); + mListener.onPreviewTextureCopied(); + mAnimState = ANIM_SWITCH_DARK_PREVIEW; + // The texture is ready. Fall through to draw darkened + // preview. + case ANIM_SWITCH_DARK_PREVIEW: + case ANIM_SWITCH_WAITING_FIRST_FRAME: + // Consume the frame. If the buffers are full, + // onFrameAvailable will not be called. Animation state + // relies on onFrameAvailable. + surfaceTexture.updateTexImage(); + mSwitchAnimManager.drawDarkPreview(canvas, x, y, width, + height, mAnimTexture); + break; + case ANIM_SWITCH_START: + mSwitchAnimManager.startAnimation(); + mAnimState = ANIM_SWITCH_RUNNING; + break; + case ANIM_CAPTURE_START: + copyPreviewTexture(canvas); + mListener.onCaptureTextureCopied(); + mCaptureAnimManager.startAnimation(x, y, width, height); + mAnimState = ANIM_CAPTURE_RUNNING; + break; + } + + if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) { + boolean drawn; + if (mAnimState == ANIM_CAPTURE_RUNNING) { + if (!mFullScreen) { + // Skip the animation if no longer in full screen mode + drawn = false; + } else { + drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture); + } + } else { + drawn = mSwitchAnimManager.drawAnimation(canvas, x, y, + width, height, this, mAnimTexture); + } + if (drawn) { + mListener.requestRender(); + } else { + // Continue to the normal draw procedure if the animation is + // not drawn. + mAnimState = ANIM_NONE; + directDraw(canvas, x, y, width, height); + } + } + callbackIfNeeded(); + } // mLock + } + + private void copyPreviewTexture(GLCanvas canvas) { + if (!mDraw.requiresSurfaceTexture() && mAnimTexture == null) { + mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true); + mAnimTexture.setIsFlippedVertically(true); + } + int width = mAnimTexture.getWidth(); + int height = mAnimTexture.getHeight(); + canvas.beginRenderTarget(mAnimTexture); + if (!mDraw.requiresSurfaceTexture()) { + mDraw.onDraw(canvas, 0, 0, width, height); + } else { + // Flip preview texture vertically. OpenGL uses bottom left point + // as the origin (0, 0). + canvas.translate(0, height); + canvas.scale(1, -1, 1); + getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix); + updateTransformMatrix(mTextureTransformMatrix); + canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height); + } + canvas.endRenderTarget(); + } + + @Override + public void noDraw() { + synchronized (mLock) { + mVisible = false; + } + } + + @Override + public void recycle() { + synchronized (mLock) { + mVisible = false; + } + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + synchronized (mLock) { + if (getSurfaceTexture() != surfaceTexture) { + return; + } + mFirstFrameArrived = true; + if (mVisible) { + if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) { + mAnimState = ANIM_SWITCH_START; + } + // We need to ask for re-render if the SurfaceTexture receives a new + // frame. + mListener.requestRender(); + } + } + } + + // We need to keep track of the size of preview frame on the screen because + // it's needed when we do switch-camera animation. See comments in + // SwitchAnimManager.java. This is based on the natural orientation, not the + // view system orientation. + public void setPreviewFrameLayoutSize(int width, int height) { + synchronized (mLock) { + mSwitchAnimManager.setPreviewFrameLayoutSize(width, height); + setPreviewLayoutSize(width, height); + } + } + + public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) { + synchronized (mLock) { + mFirstFrameArrived = false; + mOneTimeFrameDrawnListener = l; + } + } + + @Override + public SurfaceTexture getSurfaceTexture() { + synchronized (mLock) { + SurfaceTexture surfaceTexture = super.getSurfaceTexture(); + if (surfaceTexture == null && mAcquireTexture) { + try { + mLock.wait(); + surfaceTexture = super.getSurfaceTexture(); + } catch (InterruptedException e) { + Log.w(TAG, "unexpected interruption"); + } + } + return surfaceTexture; + } + } + + private void allocateTextureIfRequested(GLCanvas canvas) { + synchronized (mLock) { + if (mAcquireTexture) { + super.acquireSurfaceTexture(canvas); + mAcquireTexture = false; + mLock.notifyAll(); + } + } + } +} diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java new file mode 100644 index 000000000..3bc58a034 --- /dev/null +++ b/src/com/android/camera/CameraSettings.java @@ -0,0 +1,582 @@ +/* + * 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.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.media.CamcorderProfile; +import android.util.FloatMath; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Provides utilities and keys for Camera settings. + */ +public class CameraSettings { + private static final int NOT_FOUND = -1; + + public static final String KEY_VERSION = "pref_version_key"; + public static final String KEY_LOCAL_VERSION = "pref_local_version_key"; + public static final String KEY_RECORD_LOCATION = "pref_camera_recordlocation_key"; + public static final String KEY_VIDEO_QUALITY = "pref_video_quality_key"; + public static final String KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL = "pref_video_time_lapse_frame_interval_key"; + public static final String KEY_PICTURE_SIZE = "pref_camera_picturesize_key"; + public static final String KEY_JPEG_QUALITY = "pref_camera_jpegquality_key"; + public static final String KEY_FOCUS_MODE = "pref_camera_focusmode_key"; + public static final String KEY_FLASH_MODE = "pref_camera_flashmode_key"; + public static final String KEY_VIDEOCAMERA_FLASH_MODE = "pref_camera_video_flashmode_key"; + public static final String KEY_WHITE_BALANCE = "pref_camera_whitebalance_key"; + public static final String KEY_SCENE_MODE = "pref_camera_scenemode_key"; + public static final String KEY_EXPOSURE = "pref_camera_exposure_key"; + public static final String KEY_TIMER = "pref_camera_timer_key"; + public static final String KEY_TIMER_SOUND_EFFECTS = "pref_camera_timer_sound_key"; + public static final String KEY_VIDEO_EFFECT = "pref_video_effect_key"; + public static final String KEY_CAMERA_ID = "pref_camera_id_key"; + public static final String KEY_CAMERA_HDR = "pref_camera_hdr_key"; + public static final String KEY_CAMERA_FIRST_USE_HINT_SHOWN = "pref_camera_first_use_hint_shown_key"; + public static final String KEY_VIDEO_FIRST_USE_HINT_SHOWN = "pref_video_first_use_hint_shown_key"; + + public static final String EXPOSURE_DEFAULT_VALUE = "0"; + + public static final int CURRENT_VERSION = 5; + public static final int CURRENT_LOCAL_VERSION = 2; + + private static final String TAG = "CameraSettings"; + + private final Context mContext; + private final Parameters mParameters; + private final CameraInfo[] mCameraInfo; + private final int mCameraId; + + public CameraSettings(Activity activity, Parameters parameters, + int cameraId, CameraInfo[] cameraInfo) { + mContext = activity; + mParameters = parameters; + mCameraId = cameraId; + mCameraInfo = cameraInfo; + } + + public PreferenceGroup getPreferenceGroup(int preferenceRes) { + PreferenceInflater inflater = new PreferenceInflater(mContext); + PreferenceGroup group = + (PreferenceGroup) inflater.inflate(preferenceRes); + if (mParameters != null) initPreference(group); + return group; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + public static String getDefaultVideoQuality(int cameraId, + String defaultQuality) { + if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) { + if (CamcorderProfile.hasProfile( + cameraId, Integer.valueOf(defaultQuality))) { + return defaultQuality; + } + } + return Integer.toString(CamcorderProfile.QUALITY_HIGH); + } + + public static void initialCameraPictureSize( + Context context, Parameters parameters) { + // When launching the camera app first time, we will set the picture + // size to the first one in the list defined in "arrays.xml" and is also + // supported by the driver. + List supported = parameters.getSupportedPictureSizes(); + if (supported == null) return; + for (String candidate : context.getResources().getStringArray( + R.array.pref_camera_picturesize_entryvalues)) { + if (setCameraPictureSize(candidate, supported, parameters)) { + SharedPreferences.Editor editor = ComboPreferences + .get(context).edit(); + editor.putString(KEY_PICTURE_SIZE, candidate); + editor.apply(); + return; + } + } + Log.e(TAG, "No supported picture size found"); + } + + public static void removePreferenceFromScreen( + PreferenceGroup group, String key) { + removePreference(group, key); + } + + public static boolean setCameraPictureSize( + String candidate, List supported, Parameters parameters) { + int index = candidate.indexOf('x'); + if (index == NOT_FOUND) return false; + int width = Integer.parseInt(candidate.substring(0, index)); + int height = Integer.parseInt(candidate.substring(index + 1)); + for (Size size : supported) { + if (size.width == width && size.height == height) { + parameters.setPictureSize(width, height); + return true; + } + } + return false; + } + + public static int getMaxVideoDuration(Context context) { + int duration = 0; // in milliseconds, 0 means unlimited. + try { + duration = context.getResources().getInteger(R.integer.max_video_recording_length); + } catch (Resources.NotFoundException ex) { + } + return duration; + } + + private void initPreference(PreferenceGroup group) { + ListPreference videoQuality = group.findPreference(KEY_VIDEO_QUALITY); + ListPreference timeLapseInterval = group.findPreference(KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL); + ListPreference pictureSize = group.findPreference(KEY_PICTURE_SIZE); + ListPreference whiteBalance = group.findPreference(KEY_WHITE_BALANCE); + ListPreference sceneMode = group.findPreference(KEY_SCENE_MODE); + ListPreference flashMode = group.findPreference(KEY_FLASH_MODE); + ListPreference focusMode = group.findPreference(KEY_FOCUS_MODE); + IconListPreference exposure = + (IconListPreference) group.findPreference(KEY_EXPOSURE); + CountDownTimerPreference timer = + (CountDownTimerPreference) group.findPreference(KEY_TIMER); + ListPreference countDownSoundEffects = group.findPreference(KEY_TIMER_SOUND_EFFECTS); + IconListPreference cameraIdPref = + (IconListPreference) group.findPreference(KEY_CAMERA_ID); + ListPreference videoFlashMode = + group.findPreference(KEY_VIDEOCAMERA_FLASH_MODE); + ListPreference videoEffect = group.findPreference(KEY_VIDEO_EFFECT); + ListPreference cameraHdr = group.findPreference(KEY_CAMERA_HDR); + + // Since the screen could be loaded from different resources, we need + // to check if the preference is available here + if (videoQuality != null) { + filterUnsupportedOptions(group, videoQuality, getSupportedVideoQuality()); + } + + if (pictureSize != null) { + filterUnsupportedOptions(group, pictureSize, sizeListToStringList( + mParameters.getSupportedPictureSizes())); + filterSimilarPictureSize(group, pictureSize); + } + if (whiteBalance != null) { + filterUnsupportedOptions(group, + whiteBalance, mParameters.getSupportedWhiteBalance()); + } + if (sceneMode != null) { + filterUnsupportedOptions(group, + sceneMode, mParameters.getSupportedSceneModes()); + } + if (flashMode != null) { + filterUnsupportedOptions(group, + flashMode, mParameters.getSupportedFlashModes()); + } + if (focusMode != null) { + if (!Util.isFocusAreaSupported(mParameters)) { + filterUnsupportedOptions(group, + focusMode, mParameters.getSupportedFocusModes()); + } else { + // Remove the focus mode if we can use tap-to-focus. + removePreference(group, focusMode.getKey()); + } + } + if (videoFlashMode != null) { + filterUnsupportedOptions(group, + videoFlashMode, mParameters.getSupportedFlashModes()); + } + if (exposure != null) buildExposureCompensation(group, exposure); + if (cameraIdPref != null) buildCameraId(group, cameraIdPref); + + if (timeLapseInterval != null) { + if (ApiHelper.HAS_TIME_LAPSE_RECORDING) { + resetIfInvalid(timeLapseInterval); + } else { + removePreference(group, timeLapseInterval.getKey()); + } + } + if (videoEffect != null) { + if (ApiHelper.HAS_EFFECTS_RECORDING) { + initVideoEffect(group, videoEffect); + resetIfInvalid(videoEffect); + } else { + filterUnsupportedOptions(group, videoEffect, null); + } + } + if (cameraHdr != null && (!ApiHelper.HAS_CAMERA_HDR + || !Util.isCameraHdrSupported(mParameters))) { + removePreference(group, cameraHdr.getKey()); + } + } + + private void buildExposureCompensation( + PreferenceGroup group, IconListPreference exposure) { + int max = mParameters.getMaxExposureCompensation(); + int min = mParameters.getMinExposureCompensation(); + if (max == 0 && min == 0) { + removePreference(group, exposure.getKey()); + return; + } + float step = mParameters.getExposureCompensationStep(); + + // show only integer values for exposure compensation + int maxValue = (int) FloatMath.floor(max * step); + int minValue = (int) FloatMath.ceil(min * step); + CharSequence entries[] = new CharSequence[maxValue - minValue + 1]; + CharSequence entryValues[] = new CharSequence[maxValue - minValue + 1]; + int[] icons = new int[maxValue - minValue + 1]; + TypedArray iconIds = mContext.getResources().obtainTypedArray( + R.array.pref_camera_exposure_icons); + for (int i = minValue; i <= maxValue; ++i) { + entryValues[maxValue - i] = Integer.toString(Math.round(i / step)); + StringBuilder builder = new StringBuilder(); + if (i > 0) builder.append('+'); + entries[maxValue - i] = builder.append(i).toString(); + icons[maxValue - i] = iconIds.getResourceId(3 + i, 0); + } + exposure.setUseSingleIcon(true); + exposure.setEntries(entries); + exposure.setEntryValues(entryValues); + exposure.setLargeIconIds(icons); + } + + private void buildCameraId( + PreferenceGroup group, IconListPreference preference) { + int numOfCameras = mCameraInfo.length; + if (numOfCameras < 2) { + removePreference(group, preference.getKey()); + return; + } + + CharSequence[] entryValues = new CharSequence[numOfCameras]; + for (int i = 0; i < numOfCameras; ++i) { + entryValues[i] = "" + i; + } + preference.setEntryValues(entryValues); + } + + private static boolean removePreference(PreferenceGroup group, String key) { + for (int i = 0, n = group.size(); i < n; i++) { + CameraPreference child = group.get(i); + if (child instanceof PreferenceGroup) { + if (removePreference((PreferenceGroup) child, key)) { + return true; + } + } + if (child instanceof ListPreference && + ((ListPreference) child).getKey().equals(key)) { + group.removePreference(i); + return true; + } + } + return false; + } + + private void filterUnsupportedOptions(PreferenceGroup group, + ListPreference pref, List supported) { + + // Remove the preference if the parameter is not supported or there is + // only one options for the settings. + if (supported == null || supported.size() <= 1) { + removePreference(group, pref.getKey()); + return; + } + + pref.filterUnsupported(supported); + if (pref.getEntries().length <= 1) { + removePreference(group, pref.getKey()); + return; + } + + resetIfInvalid(pref); + } + + private void filterSimilarPictureSize(PreferenceGroup group, + ListPreference pref) { + pref.filterDuplicated(); + if (pref.getEntries().length <= 1) { + removePreference(group, pref.getKey()); + return; + } + resetIfInvalid(pref); + } + + private void resetIfInvalid(ListPreference pref) { + // Set the value to the first entry if it is invalid. + String value = pref.getValue(); + if (pref.findIndexOfValue(value) == NOT_FOUND) { + pref.setValueIndex(0); + } + } + + private static List sizeListToStringList(List sizes) { + ArrayList list = new ArrayList(); + for (Size size : sizes) { + list.add(String.format(Locale.ENGLISH, "%dx%d", size.width, size.height)); + } + return list; + } + + public static void upgradeLocalPreferences(SharedPreferences pref) { + int version; + try { + version = pref.getInt(KEY_LOCAL_VERSION, 0); + } catch (Exception ex) { + version = 0; + } + if (version == CURRENT_LOCAL_VERSION) return; + + SharedPreferences.Editor editor = pref.edit(); + if (version == 1) { + // We use numbers to represent the quality now. The quality definition is identical to + // that of CamcorderProfile.java. + editor.remove("pref_video_quality_key"); + } + editor.putInt(KEY_LOCAL_VERSION, CURRENT_LOCAL_VERSION); + editor.apply(); + } + + public static void upgradeGlobalPreferences(SharedPreferences pref) { + upgradeOldVersion(pref); + upgradeCameraId(pref); + } + + private static void upgradeOldVersion(SharedPreferences pref) { + int version; + try { + version = pref.getInt(KEY_VERSION, 0); + } catch (Exception ex) { + version = 0; + } + if (version == CURRENT_VERSION) return; + + SharedPreferences.Editor editor = pref.edit(); + if (version == 0) { + // We won't use the preference which change in version 1. + // So, just upgrade to version 1 directly + version = 1; + } + if (version == 1) { + // Change jpeg quality {65,75,85} to {normal,fine,superfine} + String quality = pref.getString(KEY_JPEG_QUALITY, "85"); + if (quality.equals("65")) { + quality = "normal"; + } else if (quality.equals("75")) { + quality = "fine"; + } else { + quality = "superfine"; + } + editor.putString(KEY_JPEG_QUALITY, quality); + version = 2; + } + if (version == 2) { + editor.putString(KEY_RECORD_LOCATION, + pref.getBoolean(KEY_RECORD_LOCATION, false) + ? RecordLocationPreference.VALUE_ON + : RecordLocationPreference.VALUE_NONE); + version = 3; + } + if (version == 3) { + // Just use video quality to replace it and + // ignore the current settings. + editor.remove("pref_camera_videoquality_key"); + editor.remove("pref_camera_video_duration_key"); + } + + editor.putInt(KEY_VERSION, CURRENT_VERSION); + editor.apply(); + } + + private static void upgradeCameraId(SharedPreferences pref) { + // The id stored in the preference may be out of range if we are running + // inside the emulator and a webcam is removed. + // Note: This method accesses the global preferences directly, not the + // combo preferences. + int cameraId = readPreferredCameraId(pref); + if (cameraId == 0) return; // fast path + + int n = CameraHolder.instance().getNumberOfCameras(); + if (cameraId < 0 || cameraId >= n) { + writePreferredCameraId(pref, 0); + } + } + + public static int readPreferredCameraId(SharedPreferences pref) { + return Integer.parseInt(pref.getString(KEY_CAMERA_ID, "0")); + } + + public static void writePreferredCameraId(SharedPreferences pref, + int cameraId) { + Editor editor = pref.edit(); + editor.putString(KEY_CAMERA_ID, Integer.toString(cameraId)); + editor.apply(); + } + + public static int readExposure(ComboPreferences preferences) { + String exposure = preferences.getString( + CameraSettings.KEY_EXPOSURE, + EXPOSURE_DEFAULT_VALUE); + try { + return Integer.parseInt(exposure); + } catch (Exception ex) { + Log.e(TAG, "Invalid exposure: " + exposure); + } + return 0; + } + + public static int readEffectType(SharedPreferences pref) { + String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none"); + if (effectSelection.equals("none")) { + return EffectsRecorder.EFFECT_NONE; + } else if (effectSelection.startsWith("goofy_face")) { + return EffectsRecorder.EFFECT_GOOFY_FACE; + } else if (effectSelection.startsWith("backdropper")) { + return EffectsRecorder.EFFECT_BACKDROPPER; + } + Log.e(TAG, "Invalid effect selection: " + effectSelection); + return EffectsRecorder.EFFECT_NONE; + } + + public static Object readEffectParameter(SharedPreferences pref) { + String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none"); + if (effectSelection.equals("none")) { + return null; + } + int separatorIndex = effectSelection.indexOf('/'); + String effectParameter = + effectSelection.substring(separatorIndex + 1); + if (effectSelection.startsWith("goofy_face")) { + if (effectParameter.equals("squeeze")) { + return EffectsRecorder.EFFECT_GF_SQUEEZE; + } else if (effectParameter.equals("big_eyes")) { + return EffectsRecorder.EFFECT_GF_BIG_EYES; + } else if (effectParameter.equals("big_mouth")) { + return EffectsRecorder.EFFECT_GF_BIG_MOUTH; + } else if (effectParameter.equals("small_mouth")) { + return EffectsRecorder.EFFECT_GF_SMALL_MOUTH; + } else if (effectParameter.equals("big_nose")) { + return EffectsRecorder.EFFECT_GF_BIG_NOSE; + } else if (effectParameter.equals("small_eyes")) { + return EffectsRecorder.EFFECT_GF_SMALL_EYES; + } + } else if (effectSelection.startsWith("backdropper")) { + // Parameter is a string that either encodes the URI to use, + // or specifies 'gallery'. + return effectParameter; + } + + Log.e(TAG, "Invalid effect selection: " + effectSelection); + return null; + } + + public static void restorePreferences(Context context, + ComboPreferences preferences, Parameters parameters) { + int currentCameraId = readPreferredCameraId(preferences); + + // Clear the preferences of both cameras. + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId != -1) { + preferences.setLocalId(context, backCameraId); + Editor editor = preferences.edit(); + editor.clear(); + editor.apply(); + } + int frontCameraId = CameraHolder.instance().getFrontCameraId(); + if (frontCameraId != -1) { + preferences.setLocalId(context, frontCameraId); + Editor editor = preferences.edit(); + editor.clear(); + editor.apply(); + } + + // Switch back to the preferences of the current camera. Otherwise, + // we may write the preference to wrong camera later. + preferences.setLocalId(context, currentCameraId); + + upgradeGlobalPreferences(preferences.getGlobal()); + upgradeLocalPreferences(preferences.getLocal()); + + // Write back the current camera id because parameters are related to + // the camera. Otherwise, we may switch to the front camera but the + // initial picture size is that of the back camera. + initialCameraPictureSize(context, parameters); + writePreferredCameraId(preferences, currentCameraId); + } + + private ArrayList getSupportedVideoQuality() { + ArrayList supported = new ArrayList(); + // Check for supported quality + if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) { + getFineResolutionQuality(supported); + } else { + supported.add(Integer.toString(CamcorderProfile.QUALITY_HIGH)); + CamcorderProfile high = CamcorderProfile.get( + mCameraId, CamcorderProfile.QUALITY_HIGH); + CamcorderProfile low = CamcorderProfile.get( + mCameraId, CamcorderProfile.QUALITY_LOW); + if (high.videoFrameHeight * high.videoFrameWidth > + low.videoFrameHeight * low.videoFrameWidth) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_LOW)); + } + } + + return supported; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private void getFineResolutionQuality(ArrayList supported) { + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_1080P)); + } + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_720P)); + } + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_480P)); + } + } + + private void initVideoEffect(PreferenceGroup group, ListPreference videoEffect) { + CharSequence[] values = videoEffect.getEntryValues(); + + boolean goofyFaceSupported = + EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_GOOFY_FACE); + boolean backdropperSupported = + EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_BACKDROPPER) && + Util.isAutoExposureLockSupported(mParameters) && + Util.isAutoWhiteBalanceLockSupported(mParameters); + + ArrayList supported = new ArrayList(); + for (CharSequence value : values) { + String effectSelection = value.toString(); + if (!goofyFaceSupported && effectSelection.startsWith("goofy_face")) continue; + if (!backdropperSupported && effectSelection.startsWith("backdropper")) continue; + supported.add(effectSelection); + } + + filterUnsupportedOptions(group, videoEffect, supported); + } +} diff --git a/src/com/android/camera/CaptureAnimManager.java b/src/com/android/camera/CaptureAnimManager.java new file mode 100644 index 000000000..64383aff7 --- /dev/null +++ b/src/com/android/camera/CaptureAnimManager.java @@ -0,0 +1,146 @@ +/* + * 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; + +import android.graphics.Color; +import android.os.SystemClock; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; + +/** + * Class to handle the capture animation. + */ +public class CaptureAnimManager { + @SuppressWarnings("unused") + private static final String TAG = "CAM_Capture"; + private static final int TIME_FLASH = 200; + private static final int TIME_HOLD = 400; + private static final int TIME_SLIDE = 400; // milliseconds. + + private static final int ANIM_BOTH = 0; + private static final int ANIM_FLASH = 1; + private static final int ANIM_SLIDE = 2; + + private final Interpolator mSlideInterpolator = new DecelerateInterpolator(); + + private int mAnimOrientation; // Could be 0, 90, 180 or 270 degrees. + private long mAnimStartTime; // milliseconds. + private float mX; // The center of the whole view including preview and review. + private float mY; + private float mDelta; + private int mDrawWidth; + private int mDrawHeight; + private int mAnimType; + + /* preview: camera preview view. + * review: view of picture just taken. + */ + public CaptureAnimManager() { + } + + public void setOrientation(int displayRotation) { + mAnimOrientation = (360 - displayRotation) % 360; + } + + public void animateSlide() { + if (mAnimType != ANIM_FLASH) { + return; + } + mAnimType = ANIM_SLIDE; + mAnimStartTime = SystemClock.uptimeMillis(); + } + + public void animateFlash() { + mAnimType = ANIM_FLASH; + } + + public void animateFlashAndSlide() { + mAnimType = ANIM_BOTH; + } + + // x, y, w and h: the rectangle area where the animation takes place. + public void startAnimation(int x, int y, int w, int h) { + mAnimStartTime = SystemClock.uptimeMillis(); + // Set the views to the initial positions. + mDrawWidth = w; + mDrawHeight = h; + mX = x; + mY = y; + switch (mAnimOrientation) { + case 0: // Preview is on the left. + mDelta = w; + break; + case 90: // Preview is below. + mDelta = -h; + break; + case 180: // Preview on the right. + mDelta = -w; + break; + case 270: // Preview is above. + mDelta = h; + break; + } + } + + // Returns true if the animation has been drawn. + public boolean drawAnimation(GLCanvas canvas, CameraScreenNail preview, + RawTexture review) { + long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime; + // Check if the animation is over + if (mAnimType == ANIM_SLIDE && timeDiff > TIME_SLIDE) return false; + if (mAnimType == ANIM_BOTH && timeDiff > TIME_HOLD + TIME_SLIDE) return false; + + int animStep = mAnimType; + if (mAnimType == ANIM_BOTH) { + animStep = (timeDiff < TIME_HOLD) ? ANIM_FLASH : ANIM_SLIDE; + if (animStep == ANIM_SLIDE) { + timeDiff -= TIME_HOLD; + } + } + + if (animStep == ANIM_FLASH) { + review.draw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight); + if (timeDiff < TIME_FLASH) { + float f = 0.3f - 0.3f * timeDiff / TIME_FLASH; + int color = Color.argb((int) (255 * f), 255, 255, 255); + canvas.fillRect(mX, mY, mDrawWidth, mDrawHeight, color); + } + } else if (animStep == ANIM_SLIDE) { + float fraction = (float) (timeDiff) / TIME_SLIDE; + float x = mX; + float y = mY; + if (mAnimOrientation == 0 || mAnimOrientation == 180) { + x = x + mDelta * mSlideInterpolator.getInterpolation(fraction); + } else { + y = y + mDelta * mSlideInterpolator.getInterpolation(fraction); + } + // float alpha = canvas.getAlpha(); + // canvas.setAlpha(fraction); + preview.directDraw(canvas, (int) mX, (int) mY, + mDrawWidth, mDrawHeight); + // canvas.setAlpha(alpha); + + review.draw(canvas, (int) x, (int) y, mDrawWidth, mDrawHeight); + } else { + return false; + } + return true; + } +} diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java new file mode 100644 index 000000000..af1476eac --- /dev/null +++ b/src/com/android/camera/ComboPreferences.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.backup.BackupManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ComboPreferences implements + SharedPreferences, + OnSharedPreferenceChangeListener { + private SharedPreferences mPrefGlobal; // global preferences + private SharedPreferences mPrefLocal; // per-camera preferences + private BackupManager mBackupManager; + private CopyOnWriteArrayList mListeners; + private static WeakHashMap sMap = + new WeakHashMap(); + + public ComboPreferences(Context context) { + mPrefGlobal = context.getSharedPreferences( + getGlobalSharedPreferencesName(context), Context.MODE_PRIVATE); + mPrefGlobal.registerOnSharedPreferenceChangeListener(this); + + synchronized (sMap) { + sMap.put(context, this); + } + mBackupManager = new BackupManager(context); + mListeners = new CopyOnWriteArrayList(); + + // The global preferences was previously stored in the default + // shared preferences file. They should be stored in the camera-specific + // shared preferences file so we can backup them solely. + SharedPreferences oldprefs = + PreferenceManager.getDefaultSharedPreferences(context); + if (!mPrefGlobal.contains(CameraSettings.KEY_VERSION) + && oldprefs.contains(CameraSettings.KEY_VERSION)) { + moveGlobalPrefsFrom(oldprefs); + } + } + + public static ComboPreferences get(Context context) { + synchronized (sMap) { + return sMap.get(context); + } + } + + private static String getLocalSharedPreferencesName( + Context context, int cameraId) { + return context.getPackageName() + "_preferences_" + cameraId; + } + + private static String getGlobalSharedPreferencesName(Context context) { + return context.getPackageName() + "_preferences_camera"; + } + + private void movePrefFrom( + Map m, String key, SharedPreferences src) { + if (m.containsKey(key)) { + Object v = m.get(key); + if (v instanceof String) { + mPrefGlobal.edit().putString(key, (String) v).apply(); + } else if (v instanceof Integer) { + mPrefGlobal.edit().putInt(key, (Integer) v).apply(); + } else if (v instanceof Long) { + mPrefGlobal.edit().putLong(key, (Long) v).apply(); + } else if (v instanceof Float) { + mPrefGlobal.edit().putFloat(key, (Float) v).apply(); + } else if (v instanceof Boolean) { + mPrefGlobal.edit().putBoolean(key, (Boolean) v).apply(); + } + src.edit().remove(key).apply(); + } + } + + private void moveGlobalPrefsFrom(SharedPreferences src) { + Map prefMap = src.getAll(); + movePrefFrom(prefMap, CameraSettings.KEY_VERSION, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, src); + movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_ID, src); + movePrefFrom(prefMap, CameraSettings.KEY_RECORD_LOCATION, src); + movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_EFFECT, src); + } + + public static String[] getSharedPreferencesNames(Context context) { + int numOfCameras = CameraHolder.instance().getNumberOfCameras(); + String prefNames[] = new String[numOfCameras + 1]; + prefNames[0] = getGlobalSharedPreferencesName(context); + for (int i = 0; i < numOfCameras; i++) { + prefNames[i + 1] = getLocalSharedPreferencesName(context, i); + } + return prefNames; + } + + // Sets the camera id and reads its preferences. Each camera has its own + // preferences. + public void setLocalId(Context context, int cameraId) { + String prefName = getLocalSharedPreferencesName(context, cameraId); + if (mPrefLocal != null) { + mPrefLocal.unregisterOnSharedPreferenceChangeListener(this); + } + mPrefLocal = context.getSharedPreferences( + prefName, Context.MODE_PRIVATE); + mPrefLocal.registerOnSharedPreferenceChangeListener(this); + } + + public SharedPreferences getGlobal() { + return mPrefGlobal; + } + + public SharedPreferences getLocal() { + return mPrefLocal; + } + + @Override + public Map getAll() { + throw new UnsupportedOperationException(); // Can be implemented if needed. + } + + private static boolean isGlobal(String key) { + return key.equals(CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL) + || key.equals(CameraSettings.KEY_CAMERA_ID) + || key.equals(CameraSettings.KEY_RECORD_LOCATION) + || key.equals(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN) + || key.equals(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN) + || key.equals(CameraSettings.KEY_VIDEO_EFFECT) + || key.equals(CameraSettings.KEY_TIMER) + || key.equals(CameraSettings.KEY_TIMER_SOUND_EFFECTS); + } + + @Override + public String getString(String key, String defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getString(key, defValue); + } else { + return mPrefLocal.getString(key, defValue); + } + } + + @Override + public int getInt(String key, int defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getInt(key, defValue); + } else { + return mPrefLocal.getInt(key, defValue); + } + } + + @Override + public long getLong(String key, long defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getLong(key, defValue); + } else { + return mPrefLocal.getLong(key, defValue); + } + } + + @Override + public float getFloat(String key, float defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getFloat(key, defValue); + } else { + return mPrefLocal.getFloat(key, defValue); + } + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getBoolean(key, defValue); + } else { + return mPrefLocal.getBoolean(key, defValue); + } + } + + // This method is not used. + @Override + public Set getStringSet(String key, Set defValues) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(String key) { + if (mPrefLocal.contains(key)) return true; + if (mPrefGlobal.contains(key)) return true; + return false; + } + + private class MyEditor implements Editor { + private Editor mEditorGlobal; + private Editor mEditorLocal; + + MyEditor() { + mEditorGlobal = mPrefGlobal.edit(); + mEditorLocal = mPrefLocal.edit(); + } + + @Override + public boolean commit() { + boolean result1 = mEditorGlobal.commit(); + boolean result2 = mEditorLocal.commit(); + return result1 && result2; + } + + @Override + public void apply() { + mEditorGlobal.apply(); + mEditorLocal.apply(); + } + + // Note: clear() and remove() affects both local and global preferences. + @Override + public Editor clear() { + mEditorGlobal.clear(); + mEditorLocal.clear(); + return this; + } + + @Override + public Editor remove(String key) { + mEditorGlobal.remove(key); + mEditorLocal.remove(key); + return this; + } + + @Override + public Editor putString(String key, String value) { + if (isGlobal(key)) { + mEditorGlobal.putString(key, value); + } else { + mEditorLocal.putString(key, value); + } + return this; + } + + @Override + public Editor putInt(String key, int value) { + if (isGlobal(key)) { + mEditorGlobal.putInt(key, value); + } else { + mEditorLocal.putInt(key, value); + } + return this; + } + + @Override + public Editor putLong(String key, long value) { + if (isGlobal(key)) { + mEditorGlobal.putLong(key, value); + } else { + mEditorLocal.putLong(key, value); + } + return this; + } + + @Override + public Editor putFloat(String key, float value) { + if (isGlobal(key)) { + mEditorGlobal.putFloat(key, value); + } else { + mEditorLocal.putFloat(key, value); + } + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + if (isGlobal(key)) { + mEditorGlobal.putBoolean(key, value); + } else { + mEditorLocal.putBoolean(key, value); + } + return this; + } + + // This method is not used. + @Override + public Editor putStringSet(String key, Set values) { + throw new UnsupportedOperationException(); + } + } + + // Note the remove() and clear() of the returned Editor may not work as + // expected because it doesn't touch the global preferences at all. + @Override + public Editor edit() { + return new MyEditor(); + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + mListeners.add(listener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + mListeners.remove(listener); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + for (OnSharedPreferenceChangeListener listener : mListeners) { + listener.onSharedPreferenceChanged(this, key); + } + mBackupManager.dataChanged(); + } +} diff --git a/src/com/android/camera/CountDownTimerPreference.java b/src/com/android/camera/CountDownTimerPreference.java new file mode 100644 index 000000000..6c0f67369 --- /dev/null +++ b/src/com/android/camera/CountDownTimerPreference.java @@ -0,0 +1,51 @@ +/* + * 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.Context; +import android.util.AttributeSet; + +import java.util.List; + +/* CountDownTimerPreference generates entries (i.e. what users see in the UI), + * and entry values (the actual value recorded in preference) in + * initCountDownTimeChoices(Context context), rather than reading the entries + * from a predefined list. When the entry values are a continuous list of numbers, + * (e.g. 0-60), it is more efficient to auto generate the list than to predefine it.*/ +public class CountDownTimerPreference extends ListPreference { + private final static int MAX_DURATION = 60; + public CountDownTimerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + initCountDownDurationChoices(context); + } + + private void initCountDownDurationChoices(Context context) { + CharSequence[] entryValues = new CharSequence[MAX_DURATION + 1]; + CharSequence[] entries = new CharSequence[MAX_DURATION + 1]; + for (int i = 0; i <= MAX_DURATION; i++) { + entryValues[i] = Integer.toString(i); + if (i == 0) { + entries[0] = context.getString(R.string.setting_off); // Off + } else { + entries[i] = context.getResources() + .getQuantityString(R.plurals.pref_camera_timer_entry, i, i); + } + } + setEntries(entries); + setEntryValues(entryValues); + } +} diff --git a/src/com/android/camera/DisableCameraReceiver.java b/src/com/android/camera/DisableCameraReceiver.java new file mode 100644 index 000000000..351740541 --- /dev/null +++ b/src/com/android/camera/DisableCameraReceiver.java @@ -0,0 +1,85 @@ +/* + * 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; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.Camera.CameraInfo; +import android.util.Log; + +// We want to disable camera-related activities if there is no camera. This +// receiver runs when BOOT_COMPLETED intent is received. After running once +// this receiver will be disabled, so it will not run again. +public class DisableCameraReceiver extends BroadcastReceiver { + private static final String TAG = "DisableCameraReceiver"; + private static final boolean CHECK_BACK_CAMERA_ONLY = true; + private static final String ACTIVITIES[] = { + "com.android.camera.CameraLauncher", + }; + + @Override + public void onReceive(Context context, Intent intent) { + // Disable camera-related activities if there is no camera. + boolean needCameraActivity = CHECK_BACK_CAMERA_ONLY + ? hasBackCamera() + : hasCamera(); + + if (!needCameraActivity) { + Log.i(TAG, "disable all camera activities"); + for (int i = 0; i < ACTIVITIES.length; i++) { + disableComponent(context, ACTIVITIES[i]); + } + } + + // Disable this receiver so it won't run again. + disableComponent(context, "com.android.camera.DisableCameraReceiver"); + } + + private boolean hasCamera() { + int n = android.hardware.Camera.getNumberOfCameras(); + Log.i(TAG, "number of camera: " + n); + return (n > 0); + } + + private boolean hasBackCamera() { + int n = android.hardware.Camera.getNumberOfCameras(); + CameraInfo info = new CameraInfo(); + for (int i = 0; i < n; i++) { + android.hardware.Camera.getCameraInfo(i, info); + if (info.facing == CameraInfo.CAMERA_FACING_BACK) { + Log.i(TAG, "back camera found: " + i); + return true; + } + } + Log.i(TAG, "no back camera"); + return false; + } + + private void disableComponent(Context context, String klass) { + ComponentName name = new ComponentName(context, klass); + PackageManager pm = context.getPackageManager(); + + // We need the DONT_KILL_APP flag, otherwise we will be killed + // immediately because we are in the same app. + pm.setComponentEnabledSetting(name, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } +} diff --git a/src/com/android/camera/EffectsRecorder.java b/src/com/android/camera/EffectsRecorder.java new file mode 100644 index 000000000..4601ab9ec --- /dev/null +++ b/src/com/android/camera/EffectsRecorder.java @@ -0,0 +1,1239 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + + +/** + * Encapsulates the mobile filter framework components needed to record video + * with effects applied. Modeled after MediaRecorder. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class EffectsRecorder { + private static final String TAG = "EffectsRecorder"; + + private static Class sClassFilter; + private static Method sFilterIsAvailable; + private static EffectsRecorder sEffectsRecorder; + // The index of the current effects recorder. + private static int sEffectsRecorderIndex; + + private static boolean sReflectionInited = false; + + private static Class sClsLearningDoneListener; + private static Class sClsOnRunnerDoneListener; + private static Class sClsOnRecordingDoneListener; + private static Class sClsSurfaceTextureSourceListener; + + private static Method sFilterSetInputValue; + + private static Constructor sCtPoint; + private static Constructor sCtQuad; + + private static Method sLearningDoneListenerOnLearningDone; + + private static Method sObjectEquals; + private static Method sObjectToString; + + private static Class sClsGraphRunner; + private static Method sGraphRunnerGetGraph; + private static Method sGraphRunnerSetDoneCallback; + private static Method sGraphRunnerRun; + private static Method sGraphRunnerGetError; + private static Method sGraphRunnerStop; + + private static Method sFilterGraphGetFilter; + private static Method sFilterGraphTearDown; + + private static Method sOnRunnerDoneListenerOnRunnerDone; + + private static Class sClsGraphEnvironment; + private static Constructor sCtGraphEnvironment; + private static Method sGraphEnvironmentCreateGLEnvironment; + private static Method sGraphEnvironmentGetRunner; + private static Method sGraphEnvironmentAddReferences; + private static Method sGraphEnvironmentLoadGraph; + private static Method sGraphEnvironmentGetContext; + + private static Method sFilterContextGetGLEnvironment; + private static Method sGLEnvironmentIsActive; + private static Method sGLEnvironmentActivate; + private static Method sGLEnvironmentDeactivate; + private static Method sSurfaceTextureTargetDisconnect; + private static Method sOnRecordingDoneListenerOnRecordingDone; + private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady; + + private Object mLearningDoneListener; + private Object mRunnerDoneCallback; + private Object mSourceReadyCallback; + // A callback to finalize the media after the recording is done. + private Object mRecordingDoneListener; + + static { + try { + sClassFilter = Class.forName("android.filterfw.core.Filter"); + sFilterIsAvailable = sClassFilter.getMethod("isAvailable", + String.class); + } catch (ClassNotFoundException ex) { + Log.v(TAG, "Can't find the class android.filterfw.core.Filter"); + } catch (NoSuchMethodException e) { + Log.v(TAG, "Can't find the method Filter.isAvailable"); + } + } + + public static final int EFFECT_NONE = 0; + public static final int EFFECT_GOOFY_FACE = 1; + public static final int EFFECT_BACKDROPPER = 2; + + public static final int EFFECT_GF_SQUEEZE = 0; + public static final int EFFECT_GF_BIG_EYES = 1; + public static final int EFFECT_GF_BIG_MOUTH = 2; + public static final int EFFECT_GF_SMALL_MOUTH = 3; + public static final int EFFECT_GF_BIG_NOSE = 4; + public static final int EFFECT_GF_SMALL_EYES = 5; + public static final int NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1; + + public static final int EFFECT_MSG_STARTED_LEARNING = 0; + public static final int EFFECT_MSG_DONE_LEARNING = 1; + public static final int EFFECT_MSG_SWITCHING_EFFECT = 2; + public static final int EFFECT_MSG_EFFECTS_STOPPED = 3; + public static final int EFFECT_MSG_RECORDING_DONE = 4; + public static final int EFFECT_MSG_PREVIEW_RUNNING = 5; + + private Context mContext; + private Handler mHandler; + + private CameraManager.CameraProxy mCameraDevice; + private CamcorderProfile mProfile; + private double mCaptureRate = 0; + private SurfaceTexture mPreviewSurfaceTexture; + private int mPreviewWidth; + private int mPreviewHeight; + private MediaRecorder.OnInfoListener mInfoListener; + private MediaRecorder.OnErrorListener mErrorListener; + + private String mOutputFile; + private FileDescriptor mFd; + private int mOrientationHint = 0; + private long mMaxFileSize = 0; + private int mMaxDurationMs = 0; + private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK; + private int mCameraDisplayOrientation; + + private int mEffect = EFFECT_NONE; + private int mCurrentEffect = EFFECT_NONE; + private EffectsListener mEffectsListener; + + private Object mEffectParameter; + + private Object mGraphEnv; + private int mGraphId; + private Object mRunner = null; + private Object mOldRunner = null; + + private SurfaceTexture mTextureSource; + + private static final int STATE_CONFIGURE = 0; + private static final int STATE_WAITING_FOR_SURFACE = 1; + private static final int STATE_STARTING_PREVIEW = 2; + private static final int STATE_PREVIEW = 3; + private static final int STATE_RECORD = 4; + private static final int STATE_RELEASED = 5; + private int mState = STATE_CONFIGURE; + + private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + private SoundClips.Player mSoundPlayer; + + /** Determine if a given effect is supported at runtime + * Some effects require libraries not available on all devices + */ + public static boolean isEffectSupported(int effectId) { + if (sFilterIsAvailable == null) return false; + + try { + switch (effectId) { + case EFFECT_GOOFY_FACE: + return (Boolean) sFilterIsAvailable.invoke(null, + "com.google.android.filterpacks.facedetect.GoofyRenderFilter"); + case EFFECT_BACKDROPPER: + return (Boolean) sFilterIsAvailable.invoke(null, + "android.filterpacks.videoproc.BackDropperFilter"); + default: + return false; + } + } catch (Exception ex) { + Log.e(TAG, "Fail to check filter", ex); + } + return false; + } + + public EffectsRecorder(Context context) { + if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")"); + + if (!sReflectionInited) { + try { + sFilterSetInputValue = sClassFilter.getMethod("setInputValue", + new Class[] {String.class, Object.class}); + + Class clsPoint = Class.forName("android.filterfw.geometry.Point"); + sCtPoint = clsPoint.getConstructor(new Class[] {float.class, + float.class}); + + Class clsQuad = Class.forName("android.filterfw.geometry.Quad"); + sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint, + clsPoint, clsPoint}); + + Class clsBackDropperFilter = Class.forName( + "android.filterpacks.videoproc.BackDropperFilter"); + sClsLearningDoneListener = Class.forName( + "android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener"); + sLearningDoneListenerOnLearningDone = sClsLearningDoneListener + .getMethod("onLearningDone", new Class[] {clsBackDropperFilter}); + + sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class}); + sObjectToString = Object.class.getMethod("toString"); + + sClsOnRunnerDoneListener = Class.forName( + "android.filterfw.core.GraphRunner$OnRunnerDoneListener"); + sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod( + "onRunnerDone", new Class[] {int.class}); + + sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner"); + sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph"); + sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod( + "setDoneCallback", new Class[] {sClsOnRunnerDoneListener}); + sGraphRunnerRun = sClsGraphRunner.getMethod("run"); + sGraphRunnerGetError = sClsGraphRunner.getMethod("getError"); + sGraphRunnerStop = sClsGraphRunner.getMethod("stop"); + + Class clsFilterContext = Class.forName("android.filterfw.core.FilterContext"); + sFilterContextGetGLEnvironment = clsFilterContext.getMethod( + "getGLEnvironment"); + + Class clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph"); + sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter", + new Class[] {String.class}); + sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown", + new Class[] {clsFilterContext}); + + sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment"); + sCtGraphEnvironment = sClsGraphEnvironment.getConstructor(); + sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod( + "createGLEnvironment"); + sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod( + "getRunner", new Class[] {int.class, int.class}); + sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod( + "addReferences", new Class[] {Object[].class}); + sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod( + "loadGraph", new Class[] {Context.class, int.class}); + sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod( + "getContext"); + + Class clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment"); + sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive"); + sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate"); + sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate"); + + Class clsSurfaceTextureTarget = Class.forName( + "android.filterpacks.videosrc.SurfaceTextureTarget"); + sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod( + "disconnect", new Class[] {clsFilterContext}); + + sClsOnRecordingDoneListener = Class.forName( + "android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener"); + sOnRecordingDoneListenerOnRecordingDone = + sClsOnRecordingDoneListener.getMethod("onRecordingDone"); + + sClsSurfaceTextureSourceListener = Class.forName( + "android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener"); + sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady = + sClsSurfaceTextureSourceListener.getMethod( + "onSurfaceTextureSourceReady", + new Class[] {SurfaceTexture.class}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + sReflectionInited = true; + } + + sEffectsRecorderIndex++; + Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex); + sEffectsRecorder = this; + SerializableInvocationHandler sih = new SerializableInvocationHandler( + sEffectsRecorderIndex); + mLearningDoneListener = Proxy.newProxyInstance( + sClsLearningDoneListener.getClassLoader(), + new Class[] {sClsLearningDoneListener}, sih); + mRunnerDoneCallback = Proxy.newProxyInstance( + sClsOnRunnerDoneListener.getClassLoader(), + new Class[] {sClsOnRunnerDoneListener}, sih); + mSourceReadyCallback = Proxy.newProxyInstance( + sClsSurfaceTextureSourceListener.getClassLoader(), + new Class[] {sClsSurfaceTextureSourceListener}, sih); + mRecordingDoneListener = Proxy.newProxyInstance( + sClsOnRecordingDoneListener.getClassLoader(), + new Class[] {sClsOnRecordingDoneListener}, sih); + + mContext = context; + mHandler = new Handler(Looper.getMainLooper()); + mSoundPlayer = SoundClips.getPlayer(context); + } + + public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) { + switch (mState) { + case STATE_PREVIEW: + throw new RuntimeException("setCamera cannot be called while previewing!"); + case STATE_RECORD: + throw new RuntimeException("setCamera cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setCamera called on an already released recorder!"); + default: + break; + } + + mCameraDevice = cameraDevice; + } + + public void setProfile(CamcorderProfile profile) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setProfile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setProfile called on an already released recorder!"); + default: + break; + } + mProfile = profile; + } + + public void setOutputFile(String outputFile) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setOutputFile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setOutputFile called on an already released recorder!"); + default: + break; + } + + mOutputFile = outputFile; + mFd = null; + } + + public void setOutputFile(FileDescriptor fd) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setOutputFile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setOutputFile called on an already released recorder!"); + default: + break; + } + + mOutputFile = null; + mFd = fd; + } + + /** + * Sets the maximum filesize (in bytes) of the recording session. + * This will be passed on to the MediaEncoderFilter and then to the + * MediaRecorder ultimately. If zero or negative, the MediaRecorder will + * disable the limit + */ + public synchronized void setMaxFileSize(long maxFileSize) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setMaxFileSize cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setMaxFileSize called on an already released recorder!"); + default: + break; + } + mMaxFileSize = maxFileSize; + } + + /** + * Sets the maximum recording duration (in ms) for the next recording session + * Setting it to zero (the default) disables the limit. + */ + public synchronized void setMaxDuration(int maxDurationMs) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setMaxDuration cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setMaxDuration called on an already released recorder!"); + default: + break; + } + mMaxDurationMs = maxDurationMs; + } + + + public void setCaptureRate(double fps) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setCaptureRate cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setCaptureRate called on an already released recorder!"); + default: + break; + } + + if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps"); + mCaptureRate = fps; + } + + public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture, + int previewWidth, + int previewHeight) { + if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")"); + switch (mState) { + case STATE_RECORD: + throw new RuntimeException( + "setPreviewSurfaceTexture cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setPreviewSurfaceTexture called on an already released recorder!"); + default: + break; + } + + mPreviewSurfaceTexture = previewSurfaceTexture; + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + + switch (mState) { + case STATE_WAITING_FOR_SURFACE: + startPreview(); + break; + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + initializeEffect(true); + break; + } + } + + public void setEffect(int effect, Object effectParameter) { + if (mLogVerbose) Log.v(TAG, + "setEffect: effect ID " + effect + + ", parameter " + effectParameter.toString()); + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setEffect cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setEffect called on an already released recorder!"); + default: + break; + } + + mEffect = effect; + mEffectParameter = effectParameter; + + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + initializeEffect(false); + } + } + + public interface EffectsListener { + public void onEffectsUpdate(int effectId, int effectMsg); + public void onEffectsError(Exception exception, String filePath); + } + + public void setEffectsListener(EffectsListener listener) { + mEffectsListener = listener; + } + + private void setFaceDetectOrientation() { + if (mCurrentEffect == EFFECT_GOOFY_FACE) { + Object rotateFilter = getGraphFilter(mRunner, "rotate"); + Object metaRotateFilter = getGraphFilter(mRunner, "metarotate"); + setInputValue(rotateFilter, "rotation", mOrientationHint); + int reverseDegrees = (360 - mOrientationHint) % 360; + setInputValue(metaRotateFilter, "rotation", reverseDegrees); + } + } + + private void setRecordingOrientation() { + if (mState != STATE_RECORD && mRunner != null) { + Object bl = newInstance(sCtPoint, new Object[] {0, 0}); + Object br = newInstance(sCtPoint, new Object[] {1, 0}); + Object tl = newInstance(sCtPoint, new Object[] {0, 1}); + Object tr = newInstance(sCtPoint, new Object[] {1, 1}); + Object recordingRegion; + if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) { + // The back camera is not mirrored, so use a identity transform + recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr}); + } else { + // Recording region needs to be tweaked for front cameras, since they + // mirror their preview + if (mOrientationHint == 0 || mOrientationHint == 180) { + // Horizontal flip in landscape + recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl}); + } else { + // Horizontal flip in portrait + recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br}); + } + } + Object recorder = getGraphFilter(mRunner, "recorder"); + setInputValue(recorder, "inputRegion", recordingRegion); + } + } + public void setOrientationHint(int degrees) { + switch (mState) { + case STATE_RELEASED: + throw new RuntimeException( + "setOrientationHint called on an already released recorder!"); + default: + break; + } + if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees); + mOrientationHint = degrees; + setFaceDetectOrientation(); + setRecordingOrientation(); + } + + public void setCameraDisplayOrientation(int orientation) { + if (mState != STATE_CONFIGURE) { + throw new RuntimeException( + "setCameraDisplayOrientation called after configuration!"); + } + mCameraDisplayOrientation = orientation; + } + + public void setCameraFacing(int facing) { + switch (mState) { + case STATE_RELEASED: + throw new RuntimeException( + "setCameraFacing called on alrady released recorder!"); + default: + break; + } + mCameraFacing = facing; + setRecordingOrientation(); + } + + public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setInfoListener cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setInfoListener called on an already released recorder!"); + default: + break; + } + mInfoListener = infoListener; + } + + public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setErrorListener cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setErrorListener called on an already released recorder!"); + default: + break; + } + mErrorListener = errorListener; + } + + private void initializeFilterFramework() { + mGraphEnv = newInstance(sCtGraphEnvironment); + invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment); + + int videoFrameWidth = mProfile.videoFrameWidth; + int videoFrameHeight = mProfile.videoFrameHeight; + if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) { + int tmp = videoFrameWidth; + videoFrameWidth = videoFrameHeight; + videoFrameHeight = tmp; + } + + invoke(mGraphEnv, sGraphEnvironmentAddReferences, + new Object[] {new Object[] { + "textureSourceCallback", mSourceReadyCallback, + "recordingWidth", videoFrameWidth, + "recordingHeight", videoFrameHeight, + "recordingProfile", mProfile, + "learningDoneListener", mLearningDoneListener, + "recordingDoneListener", mRecordingDoneListener}}); + mRunner = null; + mGraphId = -1; + mCurrentEffect = EFFECT_NONE; + } + + private synchronized void initializeEffect(boolean forceReset) { + if (forceReset || + mCurrentEffect != mEffect || + mCurrentEffect == EFFECT_BACKDROPPER) { + + invoke(mGraphEnv, sGraphEnvironmentAddReferences, + new Object[] {new Object[] { + "previewSurfaceTexture", mPreviewSurfaceTexture, + "previewWidth", mPreviewWidth, + "previewHeight", mPreviewHeight, + "orientation", mOrientationHint}}); + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects while running. Inform video camera. + sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT); + } + + switch (mEffect) { + case EFFECT_GOOFY_FACE: + mGraphId = (Integer) invoke(mGraphEnv, + sGraphEnvironmentLoadGraph, + new Object[] {mContext, R.raw.goofy_face}); + break; + case EFFECT_BACKDROPPER: + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING); + mGraphId = (Integer) invoke(mGraphEnv, + sGraphEnvironmentLoadGraph, + new Object[] {mContext, R.raw.backdropper}); + break; + default: + throw new RuntimeException("Unknown effect ID" + mEffect + "!"); + } + mCurrentEffect = mEffect; + + mOldRunner = mRunner; + mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner, + new Object[] {mGraphId, + getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")}); + invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback}); + if (mLogVerbose) { + Log.v(TAG, "New runner: " + mRunner + + ". Old runner: " + mOldRunner); + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects while running. Stop existing runner. + // The stop callback will take care of starting new runner. + mCameraDevice.stopPreview(); + mCameraDevice.setPreviewTextureAsync(null); + invoke(mOldRunner, sGraphRunnerStop); + } + } + + switch (mCurrentEffect) { + case EFFECT_GOOFY_FACE: + tryEnableVideoStabilization(true); + Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer"); + setInputValue(goofyFilter, "currentEffect", + ((Integer) mEffectParameter).intValue()); + break; + case EFFECT_BACKDROPPER: + tryEnableVideoStabilization(false); + Object backgroundSrc = getGraphFilter(mRunner, "background"); + if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) { + // Set the context first before setting sourceUrl to + // guarantee the content URI get resolved properly. + setInputValue(backgroundSrc, "context", mContext); + } + setInputValue(backgroundSrc, "sourceUrl", mEffectParameter); + // For front camera, the background video needs to be mirrored in the + // backdropper filter + if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + Object replacer = getGraphFilter(mRunner, "replacer"); + setInputValue(replacer, "mirrorBg", true); + if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored"); + } + break; + default: + break; + } + setFaceDetectOrientation(); + setRecordingOrientation(); + } + + public synchronized void startPreview() { + if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")"); + + switch (mState) { + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + // Already running preview + Log.w(TAG, "startPreview called when already running preview"); + return; + case STATE_RECORD: + throw new RuntimeException("Cannot start preview when already recording!"); + case STATE_RELEASED: + throw new RuntimeException("setEffect called on an already released recorder!"); + default: + break; + } + + if (mEffect == EFFECT_NONE) { + throw new RuntimeException("No effect selected!"); + } + if (mEffectParameter == null) { + throw new RuntimeException("No effect parameter provided!"); + } + if (mProfile == null) { + throw new RuntimeException("No recording profile provided!"); + } + if (mPreviewSurfaceTexture == null) { + if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one"); + mState = STATE_WAITING_FOR_SURFACE; + return; + } + if (mCameraDevice == null) { + throw new RuntimeException("No camera to record from!"); + } + + if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph."); + initializeFilterFramework(); + + initializeEffect(true); + + mState = STATE_STARTING_PREVIEW; + invoke(mRunner, sGraphRunnerRun); + // Rest of preview startup handled in mSourceReadyCallback + } + + private Object invokeObjectEquals(Object proxy, Object[] args) { + return Boolean.valueOf(proxy == args[0]); + } + + private Object invokeObjectToString() { + return "Proxy-" + toString(); + } + + private void invokeOnLearningDone() { + if (mLogVerbose) Log.v(TAG, "Learning done callback triggered"); + // Called in a processing thread, so have to post message back to UI + // thread + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING); + enable3ALocks(true); + } + + private void invokeOnRunnerDone(Object[] args) { + int runnerDoneResult = (Integer) args[0]; + synchronized (EffectsRecorder.this) { + if (mLogVerbose) { + Log.v(TAG, + "Graph runner done (" + EffectsRecorder.this + + ", mRunner " + mRunner + + ", mOldRunner " + mOldRunner + ")"); + } + if (runnerDoneResult == + (Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) { + // Handle error case + Log.e(TAG, "Error running filter graph!"); + Exception e = null; + if (mRunner != null) { + e = (Exception) invoke(mRunner, sGraphRunnerGetError); + } else if (mOldRunner != null) { + e = (Exception) invoke(mOldRunner, sGraphRunnerGetError); + } + raiseError(e); + } + if (mOldRunner != null) { + // Tear down old graph if available + if (mLogVerbose) Log.v(TAG, "Tearing down old graph."); + Object glEnv = getContextGLEnvironment(mGraphEnv); + if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) { + invoke(glEnv, sGLEnvironmentActivate); + } + getGraphTearDown(mOldRunner, + invoke(mGraphEnv, sGraphEnvironmentGetContext)); + if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) { + invoke(glEnv, sGLEnvironmentDeactivate); + } + mOldRunner = null; + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects, start up the new runner + if (mLogVerbose) { + Log.v(TAG, "Previous effect halted. Running graph again. state: " + + mState); + } + tryEnable3ALocks(false); + // In case of an error, the graph restarts from beginning and in case + // of the BACKDROPPER effect, the learner re-learns the background. + // Hence, we need to show the learning dialogue to the user + // to avoid recording before the learning is done. Else, the user + // could start recording before the learning is done and the new + // background comes up later leading to an end result video + // with a heterogeneous background. + // For BACKDROPPER effect, this path is also executed sometimes at + // the end of a normal recording session. In such a case, the graph + // does not restart and hence the learner does not re-learn. So we + // do not want to show the learning dialogue then. + if (runnerDoneResult == (Integer) getConstant( + sClsGraphRunner, "RESULT_ERROR") + && mCurrentEffect == EFFECT_BACKDROPPER) { + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING); + } + invoke(mRunner, sGraphRunnerRun); + } else if (mState != STATE_RELEASED) { + // Shutting down effects + if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview"); + tryEnable3ALocks(false); + sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED); + } else { + // STATE_RELEASED - camera will be/has been released as well, do nothing. + } + } + } + + private void invokeOnSurfaceTextureSourceReady(Object[] args) { + SurfaceTexture source = (SurfaceTexture) args[0]; + if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received"); + synchronized (EffectsRecorder.this) { + mTextureSource = source; + + if (mState == STATE_CONFIGURE) { + // Stop preview happened while the runner was doing startup tasks + // Since we haven't started anything up, don't do anything + // Rest of cleanup will happen in onRunnerDone + if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping."); + return; + } + if (mState == STATE_RELEASED) { + // EffectsRecorder has been released, so don't touch the camera device + // or anything else + if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping."); + return; + } + if (source == null) { + if (mLogVerbose) { + Log.v(TAG, "Ready callback: source null! Looks like graph was closed!"); + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW || + mState == STATE_RECORD) { + // A null source here means the graph is shutting down + // unexpectedly, so we need to turn off preview before + // the surface texture goes away. + if (mLogVerbose) { + Log.v(TAG, "Ready callback: State: " + mState + + ". stopCameraPreview"); + } + + stopCameraPreview(); + } + return; + } + + // Lock AE/AWB to reduce transition flicker + tryEnable3ALocks(true); + + mCameraDevice.stopPreview(); + if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview"); + mCameraDevice.setPreviewTextureAsync(mTextureSource); + + mCameraDevice.startPreviewAsync(); + + // Unlock AE/AWB after preview started + tryEnable3ALocks(false); + + mState = STATE_PREVIEW; + + if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete"); + + // Sending a message to listener that preview is complete + sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING); + } + } + + private void invokeOnRecordingDone() { + // Forward the callback to the VideoModule object (as an asynchronous event). + if (mLogVerbose) Log.v(TAG, "Recording done callback triggered"); + sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE); + } + + public synchronized void startRecording() { + if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")"); + + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("Already recording, cannot begin anew!"); + case STATE_RELEASED: + throw new RuntimeException( + "startRecording called on an already released recorder!"); + default: + break; + } + + if ((mOutputFile == null) && (mFd == null)) { + throw new RuntimeException("No output file name or descriptor provided!"); + } + + if (mState == STATE_CONFIGURE) { + startPreview(); + } + + Object recorder = getGraphFilter(mRunner, "recorder"); + if (mFd != null) { + setInputValue(recorder, "outputFileDescriptor", mFd); + } else { + setInputValue(recorder, "outputFile", mOutputFile); + } + // It is ok to set the audiosource without checking for timelapse here + // since that check will be done in the MediaEncoderFilter itself + setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER); + setInputValue(recorder, "recordingProfile", mProfile); + setInputValue(recorder, "orientationHint", mOrientationHint); + // Important to set the timelapseinterval to 0 if the capture rate is not >0 + // since the recorder does not get created every time the recording starts. + // The recorder infers whether the capture is timelapsed based on the value of + // this interval + boolean captureTimeLapse = mCaptureRate > 0; + if (captureTimeLapse) { + double timeBetweenFrameCapture = 1 / mCaptureRate; + setInputValue(recorder, "timelapseRecordingIntervalUs", + (long) (1000000 * timeBetweenFrameCapture)); + + } else { + setInputValue(recorder, "timelapseRecordingIntervalUs", 0L); + } + + if (mInfoListener != null) { + setInputValue(recorder, "infoListener", mInfoListener); + } + if (mErrorListener != null) { + setInputValue(recorder, "errorListener", mErrorListener); + } + setInputValue(recorder, "maxFileSize", mMaxFileSize); + setInputValue(recorder, "maxDurationMs", mMaxDurationMs); + setInputValue(recorder, "recording", true); + mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); + mState = STATE_RECORD; + } + + public synchronized void stopRecording() { + if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")"); + + switch (mState) { + case STATE_CONFIGURE: + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + Log.w(TAG, "StopRecording called when recording not active!"); + return; + case STATE_RELEASED: + throw new RuntimeException("stopRecording called on released EffectsRecorder!"); + default: + break; + } + Object recorder = getGraphFilter(mRunner, "recorder"); + setInputValue(recorder, "recording", false); + mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); + mState = STATE_PREVIEW; + } + + // Called to tell the filter graph that the display surfacetexture is not valid anymore. + // So the filter graph should not hold any reference to the surface created with that. + public synchronized void disconnectDisplay() { + if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " + + "SurfaceTexture"); + Object display = getGraphFilter(mRunner, "display"); + invoke(display, sSurfaceTextureTargetDisconnect, new Object[] { + invoke(mGraphEnv, sGraphEnvironmentGetContext)}); + } + + // The VideoModule will call this to notify that the camera is being + // released to the outside world. This call should happen after the + // stopRecording call. Else, the effects may throw an exception. + // With the recording stopped, the stopPreview call will not try to + // release the camera again. + // This must be called in onPause() if the effects are ON. + public synchronized void disconnectCamera() { + if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera"); + stopCameraPreview(); + mCameraDevice = null; + } + + // In a normal case, when the disconnect is not called, we should not + // set the camera device to null, since on return callback, we try to + // enable 3A locks, which need the cameradevice. + public synchronized void stopCameraPreview() { + if (mLogVerbose) Log.v(TAG, "Stopping camera preview."); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Nothing to disconnect"); + return; + } + mCameraDevice.stopPreview(); + mCameraDevice.setPreviewTextureAsync(null); + } + + // Stop and release effect resources + public synchronized void stopPreview() { + if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")"); + switch (mState) { + case STATE_CONFIGURE: + Log.w(TAG, "StopPreview called when preview not active!"); + return; + case STATE_RELEASED: + throw new RuntimeException("stopPreview called on released EffectsRecorder!"); + default: + break; + } + + if (mState == STATE_RECORD) { + stopRecording(); + } + + mCurrentEffect = EFFECT_NONE; + + // This will not do anything if the camera has already been disconnected. + stopCameraPreview(); + + mState = STATE_CONFIGURE; + mOldRunner = mRunner; + invoke(mRunner, sGraphRunnerStop); + mRunner = null; + // Rest of stop and release handled in mRunnerDoneCallback + } + + // Try to enable/disable video stabilization if supported; otherwise return false + // It is called from a synchronized block. + boolean tryEnableVideoStabilization(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization."); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not enabling video stabilization."); + return false; + } + Camera.Parameters params = mCameraDevice.getParameters(); + + String vstabSupported = params.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle); + params.set("video-stabilization", toggle ? "true" : "false"); + mCameraDevice.setParameters(params); + return true; + } + if (mLogVerbose) Log.v(TAG, "Video stabilization not supported"); + return false; + } + + // Try to enable/disable 3A locks if supported; otherwise return false + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + synchronized boolean tryEnable3ALocks(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks"); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not tryenabling 3A locks."); + return false; + } + Camera.Parameters params = mCameraDevice.getParameters(); + if (Util.isAutoExposureLockSupported(params) && + Util.isAutoWhiteBalanceLockSupported(params)) { + params.setAutoExposureLock(toggle); + params.setAutoWhiteBalanceLock(toggle); + mCameraDevice.setParameters(params); + return true; + } + return false; + } + + // Try to enable/disable 3A locks if supported; otherwise, throw error + // Use this when locks are essential to success + synchronized void enable3ALocks(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "Enable3ALocks"); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not enabling 3A locks."); + return; + } + Camera.Parameters params = mCameraDevice.getParameters(); + if (!tryEnable3ALocks(toggle)) { + throw new RuntimeException("Attempt to lock 3A on camera with no locking support!"); + } + } + + static class SerializableInvocationHandler + implements InvocationHandler, Serializable { + private final int mEffectsRecorderIndex; + public SerializableInvocationHandler(int index) { + mEffectsRecorderIndex = index; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (sEffectsRecorder == null) return null; + if (mEffectsRecorderIndex != sEffectsRecorderIndex) { + Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex); + return null; + } + if (method.equals(sObjectEquals)) { + return sEffectsRecorder.invokeObjectEquals(proxy, args); + } else if (method.equals(sObjectToString)) { + return sEffectsRecorder.invokeObjectToString(); + } else if (method.equals(sLearningDoneListenerOnLearningDone)) { + sEffectsRecorder.invokeOnLearningDone(); + } else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) { + sEffectsRecorder.invokeOnRunnerDone(args); + } else if (method.equals( + sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) { + sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args); + } else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) { + sEffectsRecorder.invokeOnRecordingDone(); + } + return null; + } + } + + // Indicates that all camera/recording activity needs to halt + public synchronized void release() { + if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")"); + + switch (mState) { + case STATE_RECORD: + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + stopPreview(); + // Fall-through + default: + if (mSoundPlayer != null) { + mSoundPlayer.release(); + mSoundPlayer = null; + } + mState = STATE_RELEASED; + break; + } + sEffectsRecorder = null; + } + + private void sendMessage(final int effect, final int msg) { + if (mEffectsListener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + mEffectsListener.onEffectsUpdate(effect, msg); + } + }); + } + } + + private void raiseError(final Exception exception) { + if (mEffectsListener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mFd != null) { + mEffectsListener.onEffectsError(exception, null); + } else { + mEffectsListener.onEffectsError(exception, mOutputFile); + } + } + }); + } + } + + // invoke method on receiver with no arguments + private Object invoke(Object receiver, Method method) { + try { + return method.invoke(receiver); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + // invoke method on receiver with arguments + private Object invoke(Object receiver, Method method, Object[] args) { + try { + return method.invoke(receiver, args); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void setInputValue(Object receiver, String key, Object value) { + try { + sFilterSetInputValue.invoke(receiver, new Object[] {key, value}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object newInstance(Constructor ct, Object[] initArgs) { + try { + return ct.newInstance(initArgs); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object newInstance(Constructor ct) { + try { + return ct.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getGraphFilter(Object receiver, String name) { + try { + return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph + .invoke(receiver), new Object[] {name}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getContextGLEnvironment(Object receiver) { + try { + return sFilterContextGetGLEnvironment + .invoke(sGraphEnvironmentGetContext.invoke(receiver)); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void getGraphTearDown(Object receiver, Object filterContext) { + try { + sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver), + new Object[]{filterContext}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getConstant(Class cls, String name) { + try { + return cls.getDeclaredField(name).get(null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java new file mode 100644 index 000000000..605556599 --- /dev/null +++ b/src/com/android/camera/Exif.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.util.Log; + +import com.android.gallery3d.exif.ExifInvalidFormatException; +import com.android.gallery3d.exif.ExifParser; +import com.android.gallery3d.exif.ExifTag; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class Exif { + private static final String TAG = "CameraExif"; + + // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + public static int getOrientation(byte[] jpeg) { + if (jpeg == null) return 0; + + InputStream is = new ByteArrayInputStream(jpeg); + + try { + ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); + int event = parser.next(); + while(event != ExifParser.EVENT_END) { + if (event == ExifParser.EVENT_NEW_TAG) { + ExifTag tag = parser.getTag(); + if (tag.getTagId() == ExifTag.TAG_ORIENTATION && + tag.hasValue()) { + int orient = (int) tag.getValueAt(0); + switch (orient) { + case ExifTag.Orientation.TOP_LEFT: + return 0; + case ExifTag.Orientation.BOTTOM_LEFT: + return 180; + case ExifTag.Orientation.RIGHT_TOP: + return 90; + case ExifTag.Orientation.RIGHT_BOTTOM: + return 270; + default: + Log.i(TAG, "Unsupported orientation"); + return 0; + } + } + } + event = parser.next(); + } + Log.i(TAG, "Orientation not found"); + return 0; + } catch (IOException e) { + Log.w(TAG, "Failed to read EXIF orientation", e); + return 0; + } catch (ExifInvalidFormatException e) { + Log.w(TAG, "Failed to read EXIF orientation", e); + return 0; + } + } +} diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java new file mode 100644 index 000000000..2bec18760 --- /dev/null +++ b/src/com/android/camera/FocusOverlayManager.java @@ -0,0 +1,560 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera.Area; +import android.hardware.Camera.Parameters; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.camera.ui.FaceView; +import com.android.camera.ui.FocusIndicator; +import com.android.camera.ui.PieRenderer; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.List; + +/* A class that handles everything about focus in still picture mode. + * This also handles the metering area because it is the same as focus area. + * + * The test cases: + * (1) The camera has continuous autofocus. Move the camera. Take a picture when + * CAF is not in progress. + * (2) The camera has continuous autofocus. Move the camera. Take a picture when + * CAF is in progress. + * (3) The camera has face detection. Point the camera at some faces. Hold the + * shutter. Release to take a picture. + * (4) The camera has face detection. Point the camera at some faces. Single tap + * the shutter to take a picture. + * (5) The camera has autofocus. Single tap the shutter to take a picture. + * (6) The camera has autofocus. Hold the shutter. Release to take a picture. + * (7) The camera has no autofocus. Single tap the shutter and take a picture. + * (8) The camera has autofocus and supports focus area. Touch the screen to + * trigger autofocus. Take a picture. + * (9) The camera has autofocus and supports focus area. Touch the screen to + * trigger autofocus. Wait until it times out. + * (10) The camera has no autofocus and supports metering area. Touch the screen + * to change metering area. + */ +public class FocusOverlayManager { + private static final String TAG = "CAM_FocusManager"; + + private static final int RESET_TOUCH_FOCUS = 0; + private static final int RESET_TOUCH_FOCUS_DELAY = 3000; + + private int mState = STATE_IDLE; + private static final int STATE_IDLE = 0; // Focus is not active. + private static final int STATE_FOCUSING = 1; // Focus is in progress. + // Focus is in progress and the camera should take a picture after focus finishes. + private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2; + private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds. + private static final int STATE_FAIL = 4; // Focus finishes and fails. + + private boolean mInitialized; + private boolean mFocusAreaSupported; + private boolean mMeteringAreaSupported; + private boolean mLockAeAwbNeeded; + private boolean mAeAwbLock; + private Matrix mMatrix; + + private PieRenderer mPieRenderer; + + private int mPreviewWidth; // The width of the preview frame layout. + private int mPreviewHeight; // The height of the preview frame layout. + private boolean mMirror; // true if the camera is front-facing. + private int mDisplayOrientation; + private FaceView mFaceView; + private List mFocusArea; // focus area in driver format + private List mMeteringArea; // metering area in driver format + private String mFocusMode; + private String[] mDefaultFocusModes; + private String mOverrideFocusMode; + private Parameters mParameters; + private ComboPreferences mPreferences; + private Handler mHandler; + Listener mListener; + + public interface Listener { + public void autoFocus(); + public void cancelAutoFocus(); + public boolean capture(); + public void startFaceDetection(); + public void stopFaceDetection(); + public void setFocusParameters(); + } + + private class MainHandler extends Handler { + public MainHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case RESET_TOUCH_FOCUS: { + cancelAutoFocus(); + mListener.startFaceDetection(); + break; + } + } + } + } + + public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes, + Parameters parameters, Listener listener, + boolean mirror, Looper looper) { + mHandler = new MainHandler(looper); + mMatrix = new Matrix(); + mPreferences = preferences; + mDefaultFocusModes = defaultFocusModes; + setParameters(parameters); + mListener = listener; + setMirror(mirror); + } + + public void setFocusRenderer(PieRenderer renderer) { + mPieRenderer = renderer; + mInitialized = (mMatrix != null); + } + + public void setParameters(Parameters parameters) { + // parameters can only be null when onConfigurationChanged is called + // before camera is open. We will just return in this case, because + // parameters will be set again later with the right parameters after + // camera is open. + if (parameters == null) return; + mParameters = parameters; + mFocusAreaSupported = Util.isFocusAreaSupported(parameters); + mMeteringAreaSupported = Util.isMeteringAreaSupported(parameters); + mLockAeAwbNeeded = (Util.isAutoExposureLockSupported(mParameters) || + Util.isAutoWhiteBalanceLockSupported(mParameters)); + } + + public void setPreviewSize(int previewWidth, int previewHeight) { + if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) { + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + setMatrix(); + } + } + + public void setMirror(boolean mirror) { + mMirror = mirror; + setMatrix(); + } + + public void setDisplayOrientation(int displayOrientation) { + mDisplayOrientation = displayOrientation; + setMatrix(); + } + + public void setFaceView(FaceView faceView) { + mFaceView = faceView; + } + + private void setMatrix() { + if (mPreviewWidth != 0 && mPreviewHeight != 0) { + Matrix matrix = new Matrix(); + Util.prepareMatrix(matrix, mMirror, mDisplayOrientation, + mPreviewWidth, mPreviewHeight); + // In face detection, the matrix converts the driver coordinates to UI + // coordinates. In tap focus, the inverted matrix converts the UI + // coordinates to driver coordinates. + matrix.invert(mMatrix); + mInitialized = (mPieRenderer != null); + } + } + + private void lockAeAwbIfNeeded() { + if (mLockAeAwbNeeded && !mAeAwbLock) { + mAeAwbLock = true; + mListener.setFocusParameters(); + } + } + + private void unlockAeAwbIfNeeded() { + if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) { + mAeAwbLock = false; + mListener.setFocusParameters(); + } + } + + public void onShutterDown() { + if (!mInitialized) return; + + boolean autoFocusCalled = false; + if (needAutoFocusCall()) { + // Do not focus if touch focus has been triggered. + if (mState != STATE_SUCCESS && mState != STATE_FAIL) { + autoFocus(); + autoFocusCalled = true; + } + } + + if (!autoFocusCalled) lockAeAwbIfNeeded(); + } + + public void onShutterUp() { + if (!mInitialized) return; + + if (needAutoFocusCall()) { + // User releases half-pressed focus key. + if (mState == STATE_FOCUSING || mState == STATE_SUCCESS + || mState == STATE_FAIL) { + cancelAutoFocus(); + } + } + + // Unlock AE and AWB after cancelAutoFocus. Camera API does not + // guarantee setParameters can be called during autofocus. + unlockAeAwbIfNeeded(); + } + + public void doSnap() { + if (!mInitialized) return; + + // If the user has half-pressed the shutter and focus is completed, we + // can take the photo right away. If the focus mode is infinity, we can + // also take the photo. + if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) { + capture(); + } else if (mState == STATE_FOCUSING) { + // Half pressing the shutter (i.e. the focus button event) will + // already have requested AF for us, so just request capture on + // focus here. + mState = STATE_FOCUSING_SNAP_ON_FINISH; + } else if (mState == STATE_IDLE) { + // We didn't do focus. This can happen if the user press focus key + // while the snapshot is still in progress. The user probably wants + // the next snapshot as soon as possible, so we just do a snapshot + // without focusing again. + capture(); + } + } + + public void onAutoFocus(boolean focused, boolean shutterButtonPressed) { + if (mState == STATE_FOCUSING_SNAP_ON_FINISH) { + // Take the picture no matter focus succeeds or fails. No need + // to play the AF sound if we're about to play the shutter + // sound. + if (focused) { + mState = STATE_SUCCESS; + } else { + mState = STATE_FAIL; + } + updateFocusUI(); + capture(); + } else if (mState == STATE_FOCUSING) { + // This happens when (1) user is half-pressing the focus key or + // (2) touch focus is triggered. Play the focus tone. Do not + // take the picture now. + if (focused) { + mState = STATE_SUCCESS; + } else { + mState = STATE_FAIL; + } + updateFocusUI(); + // If this is triggered by touch focus, cancel focus after a + // while. + if (mFocusArea != null) { + mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); + } + if (shutterButtonPressed) { + // Lock AE & AWB so users can half-press shutter and recompose. + lockAeAwbIfNeeded(); + } + } else if (mState == STATE_IDLE) { + // User has released the focus key before focus completes. + // Do nothing. + } + } + + public void onAutoFocusMoving(boolean moving) { + if (!mInitialized) return; + // Ignore if the camera has detected some faces. + if (mFaceView != null && mFaceView.faceExists()) { + mPieRenderer.clear(); + return; + } + + // Ignore if we have requested autofocus. This method only handles + // continuous autofocus. + if (mState != STATE_IDLE) return; + + if (moving) { + mPieRenderer.showStart(); + } else { + mPieRenderer.showSuccess(true); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void initializeFocusAreas(int focusWidth, int focusHeight, + int x, int y, int previewWidth, int previewHeight) { + if (mFocusArea == null) { + mFocusArea = new ArrayList(); + mFocusArea.add(new Area(new Rect(), 1)); + } + + // Convert the coordinates to driver format. + calculateTapArea(focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, + ((Area) mFocusArea.get(0)).rect); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void initializeMeteringAreas(int focusWidth, int focusHeight, + int x, int y, int previewWidth, int previewHeight) { + if (mMeteringArea == null) { + mMeteringArea = new ArrayList(); + mMeteringArea.add(new Area(new Rect(), 1)); + } + + // Convert the coordinates to driver format. + // AE area is bigger because exposure is sensitive and + // easy to over- or underexposure if area is too small. + calculateTapArea(focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight, + ((Area) mMeteringArea.get(0)).rect); + } + + public void onSingleTapUp(int x, int y) { + if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) return; + + // Let users be able to cancel previous touch focus. + if ((mFocusArea != null) && (mState == STATE_FOCUSING || + mState == STATE_SUCCESS || mState == STATE_FAIL)) { + cancelAutoFocus(); + } + // Initialize variables. + int focusWidth = mPieRenderer.getSize(); + int focusHeight = mPieRenderer.getSize(); + if (focusWidth == 0 || mPieRenderer.getWidth() == 0 + || mPieRenderer.getHeight() == 0) return; + int previewWidth = mPreviewWidth; + int previewHeight = mPreviewHeight; + // Initialize mFocusArea. + if (mFocusAreaSupported) { + initializeFocusAreas( + focusWidth, focusHeight, x, y, previewWidth, previewHeight); + } + // Initialize mMeteringArea. + if (mMeteringAreaSupported) { + initializeMeteringAreas( + focusWidth, focusHeight, x, y, previewWidth, previewHeight); + } + + // Use margin to set the focus indicator to the touched area. + mPieRenderer.setFocus(x, y); + + // Stop face detection because we want to specify focus and metering area. + mListener.stopFaceDetection(); + + // Set the focus area and metering area. + mListener.setFocusParameters(); + if (mFocusAreaSupported) { + autoFocus(); + } else { // Just show the indicator in all other cases. + updateFocusUI(); + // Reset the metering area in 3 seconds. + mHandler.removeMessages(RESET_TOUCH_FOCUS); + mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); + } + } + + public void onPreviewStarted() { + mState = STATE_IDLE; + } + + public void onPreviewStopped() { + // If auto focus was in progress, it would have been stopped. + mState = STATE_IDLE; + resetTouchFocus(); + updateFocusUI(); + } + + public void onCameraReleased() { + onPreviewStopped(); + } + + private void autoFocus() { + Log.v(TAG, "Start autofocus."); + mListener.autoFocus(); + mState = STATE_FOCUSING; + // Pause the face view because the driver will keep sending face + // callbacks after the focus completes. + if (mFaceView != null) mFaceView.pause(); + updateFocusUI(); + mHandler.removeMessages(RESET_TOUCH_FOCUS); + } + + private void cancelAutoFocus() { + Log.v(TAG, "Cancel autofocus."); + + // Reset the tap area before calling mListener.cancelAutofocus. + // Otherwise, focus mode stays at auto and the tap area passed to the + // driver is not reset. + resetTouchFocus(); + mListener.cancelAutoFocus(); + if (mFaceView != null) mFaceView.resume(); + mState = STATE_IDLE; + updateFocusUI(); + mHandler.removeMessages(RESET_TOUCH_FOCUS); + } + + private void capture() { + if (mListener.capture()) { + mState = STATE_IDLE; + mHandler.removeMessages(RESET_TOUCH_FOCUS); + } + } + + public String getFocusMode() { + if (mOverrideFocusMode != null) return mOverrideFocusMode; + List supportedFocusModes = mParameters.getSupportedFocusModes(); + + if (mFocusAreaSupported && mFocusArea != null) { + // Always use autofocus in tap-to-focus. + mFocusMode = Parameters.FOCUS_MODE_AUTO; + } else { + // The default is continuous autofocus. + mFocusMode = mPreferences.getString( + CameraSettings.KEY_FOCUS_MODE, null); + + // Try to find a supported focus mode from the default list. + if (mFocusMode == null) { + for (int i = 0; i < mDefaultFocusModes.length; i++) { + String mode = mDefaultFocusModes[i]; + if (Util.isSupported(mode, supportedFocusModes)) { + mFocusMode = mode; + break; + } + } + } + } + if (!Util.isSupported(mFocusMode, supportedFocusModes)) { + // For some reasons, the driver does not support the current + // focus mode. Fall back to auto. + if (Util.isSupported(Parameters.FOCUS_MODE_AUTO, + mParameters.getSupportedFocusModes())) { + mFocusMode = Parameters.FOCUS_MODE_AUTO; + } else { + mFocusMode = mParameters.getFocusMode(); + } + } + return mFocusMode; + } + + public List getFocusAreas() { + return mFocusArea; + } + + public List getMeteringAreas() { + return mMeteringArea; + } + + public void updateFocusUI() { + if (!mInitialized) return; + // Show only focus indicator or face indicator. + boolean faceExists = (mFaceView != null && mFaceView.faceExists()); + FocusIndicator focusIndicator = (faceExists) ? mFaceView : mPieRenderer; + + if (mState == STATE_IDLE) { + if (mFocusArea == null) { + focusIndicator.clear(); + } else { + // Users touch on the preview and the indicator represents the + // metering area. Either focus area is not supported or + // autoFocus call is not required. + focusIndicator.showStart(); + } + } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) { + focusIndicator.showStart(); + } else { + if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) { + // TODO: check HAL behavior and decide if this can be removed. + focusIndicator.showSuccess(false); + } else if (mState == STATE_SUCCESS) { + focusIndicator.showSuccess(false); + } else if (mState == STATE_FAIL) { + focusIndicator.showFail(false); + } + } + } + + public void resetTouchFocus() { + if (!mInitialized) return; + + // Put focus indicator to the center. clear reset position + mPieRenderer.clear(); + + mFocusArea = null; + mMeteringArea = null; + } + + private void calculateTapArea(int focusWidth, int focusHeight, float areaMultiple, + int x, int y, int previewWidth, int previewHeight, Rect rect) { + int areaWidth = (int) (focusWidth * areaMultiple); + int areaHeight = (int) (focusHeight * areaMultiple); + int left = Util.clamp(x - areaWidth / 2, 0, previewWidth - areaWidth); + int top = Util.clamp(y - areaHeight / 2, 0, previewHeight - areaHeight); + + RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight); + mMatrix.mapRect(rectF); + Util.rectFToRect(rectF, rect); + } + + /* package */ int getFocusState() { + return mState; + } + + public boolean isFocusCompleted() { + return mState == STATE_SUCCESS || mState == STATE_FAIL; + } + + public boolean isFocusingSnapOnFinish() { + return mState == STATE_FOCUSING_SNAP_ON_FINISH; + } + + public void removeMessages() { + mHandler.removeMessages(RESET_TOUCH_FOCUS); + } + + public void overrideFocusMode(String focusMode) { + mOverrideFocusMode = focusMode; + } + + public void setAeAwbLock(boolean lock) { + mAeAwbLock = lock; + } + + public boolean getAeAwbLock() { + return mAeAwbLock; + } + + private boolean needAutoFocusCall() { + String focusMode = getFocusMode(); + return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY) + || focusMode.equals(Parameters.FOCUS_MODE_FIXED) + || focusMode.equals(Parameters.FOCUS_MODE_EDOF)); + } +} diff --git a/src/com/android/camera/IconListPreference.java b/src/com/android/camera/IconListPreference.java new file mode 100644 index 000000000..6bcd59df1 --- /dev/null +++ b/src/com/android/camera/IconListPreference.java @@ -0,0 +1,115 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import java.util.List; + +/** A {@code ListPreference} where each entry has a corresponding icon. */ +public class IconListPreference extends ListPreference { + private int mSingleIconId; + private int mIconIds[]; + private int mLargeIconIds[]; + private int mImageIds[]; + private boolean mUseSingleIcon; + + public IconListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.IconListPreference, 0, 0); + Resources res = context.getResources(); + mSingleIconId = a.getResourceId( + R.styleable.IconListPreference_singleIcon, 0); + mIconIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_icons, 0)); + mLargeIconIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_largeIcons, 0)); + mImageIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_images, 0)); + a.recycle(); + } + + public int getSingleIcon() { + return mSingleIconId; + } + + public int[] getIconIds() { + return mIconIds; + } + + public int[] getLargeIconIds() { + return mLargeIconIds; + } + + public int[] getImageIds() { + return mImageIds; + } + + public boolean getUseSingleIcon() { + return mUseSingleIcon; + } + + public void setIconIds(int[] iconIds) { + mIconIds = iconIds; + } + + public void setLargeIconIds(int[] largeIconIds) { + mLargeIconIds = largeIconIds; + } + + public void setUseSingleIcon(boolean useSingle) { + mUseSingleIcon = useSingle; + } + + private int[] getIds(Resources res, int iconsRes) { + if (iconsRes == 0) return null; + TypedArray array = res.obtainTypedArray(iconsRes); + int n = array.length(); + int ids[] = new int[n]; + for (int i = 0; i < n; ++i) { + ids[i] = array.getResourceId(i, 0); + } + array.recycle(); + return ids; + } + + @Override + public void filterUnsupported(List supported) { + CharSequence entryValues[] = getEntryValues(); + IntArray iconIds = new IntArray(); + IntArray largeIconIds = new IntArray(); + IntArray imageIds = new IntArray(); + + for (int i = 0, len = entryValues.length; i < len; i++) { + if (supported.indexOf(entryValues[i].toString()) >= 0) { + if (mIconIds != null) iconIds.add(mIconIds[i]); + if (mLargeIconIds != null) largeIconIds.add(mLargeIconIds[i]); + if (mImageIds != null) imageIds.add(mImageIds[i]); + } + } + if (mIconIds != null) mIconIds = iconIds.toArray(new int[iconIds.size()]); + if (mLargeIconIds != null) { + mLargeIconIds = largeIconIds.toArray(new int[largeIconIds.size()]); + } + if (mImageIds != null) mImageIds = imageIds.toArray(new int[imageIds.size()]); + super.filterUnsupported(supported); + } +} diff --git a/src/com/android/camera/IntArray.java b/src/com/android/camera/IntArray.java new file mode 100644 index 000000000..a2550dbd8 --- /dev/null +++ b/src/com/android/camera/IntArray.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int size() { + return mSize; + } + + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } +} diff --git a/src/com/android/camera/ListPreference.java b/src/com/android/camera/ListPreference.java new file mode 100644 index 000000000..17266ea73 --- /dev/null +++ b/src/com/android/camera/ListPreference.java @@ -0,0 +1,181 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; + +import java.util.ArrayList; +import java.util.List; + +/** + * A type of CameraPreference whose number of possible values + * is limited. + */ +public class ListPreference extends CameraPreference { + private static final String TAG = "ListPreference"; + private final String mKey; + private String mValue; + private final CharSequence[] mDefaultValues; + + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private boolean mLoaded = false; + + public ListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ListPreference, 0, 0); + + mKey = Util.checkNotNull( + a.getString(R.styleable.ListPreference_key)); + + // We allow the defaultValue attribute to be a string or an array of + // strings. The reason we need multiple default values is that some + // of them may be unsupported on a specific platform (for example, + // continuous auto-focus). In that case the first supported value + // in the array will be used. + int attrDefaultValue = R.styleable.ListPreference_defaultValue; + TypedValue tv = a.peekValue(attrDefaultValue); + if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { + mDefaultValues = a.getTextArray(attrDefaultValue); + } else { + mDefaultValues = new CharSequence[1]; + mDefaultValues[0] = a.getString(attrDefaultValue); + } + + setEntries(a.getTextArray(R.styleable.ListPreference_entries)); + setEntryValues(a.getTextArray( + R.styleable.ListPreference_entryValues)); + a.recycle(); + } + + public String getKey() { + return mKey; + } + + public CharSequence[] getEntries() { + return mEntries; + } + + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + public void setEntries(CharSequence entries[]) { + mEntries = entries == null ? new CharSequence[0] : entries; + } + + public void setEntryValues(CharSequence values[]) { + mEntryValues = values == null ? new CharSequence[0] : values; + } + + public String getValue() { + if (!mLoaded) { + mValue = getSharedPreferences().getString(mKey, + findSupportedDefaultValue()); + mLoaded = true; + } + return mValue; + } + + // Find the first value in mDefaultValues which is supported. + private String findSupportedDefaultValue() { + for (int i = 0; i < mDefaultValues.length; i++) { + for (int j = 0; j < mEntryValues.length; j++) { + // Note that mDefaultValues[i] may be null (if unspecified + // in the xml file). + if (mEntryValues[j].equals(mDefaultValues[i])) { + return mDefaultValues[i].toString(); + } + } + } + return null; + } + + public void setValue(String value) { + if (findIndexOfValue(value) < 0) throw new IllegalArgumentException(); + mValue = value; + persistStringValue(value); + } + + public void setValueIndex(int index) { + setValue(mEntryValues[index].toString()); + } + + public int findIndexOfValue(String value) { + for (int i = 0, n = mEntryValues.length; i < n; ++i) { + if (Util.equals(mEntryValues[i], value)) return i; + } + return -1; + } + + public String getEntry() { + return mEntries[findIndexOfValue(getValue())].toString(); + } + + protected void persistStringValue(String value) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.putString(mKey, value); + editor.apply(); + } + + @Override + public void reloadValue() { + this.mLoaded = false; + } + + public void filterUnsupported(List supported) { + ArrayList entries = new ArrayList(); + ArrayList entryValues = new ArrayList(); + for (int i = 0, len = mEntryValues.length; i < len; i++) { + if (supported.indexOf(mEntryValues[i].toString()) >= 0) { + entries.add(mEntries[i]); + entryValues.add(mEntryValues[i]); + } + } + int size = entries.size(); + mEntries = entries.toArray(new CharSequence[size]); + mEntryValues = entryValues.toArray(new CharSequence[size]); + } + + public void filterDuplicated() { + ArrayList entries = new ArrayList(); + ArrayList entryValues = new ArrayList(); + for (int i = 0, len = mEntryValues.length; i < len; i++) { + if (!entries.contains(mEntries[i])) { + entries.add(mEntries[i]); + entryValues.add(mEntryValues[i]); + } + } + int size = entries.size(); + mEntries = entries.toArray(new CharSequence[size]); + mEntryValues = entryValues.toArray(new CharSequence[size]); + } + + public void print() { + Log.v(TAG, "Preference key=" + getKey() + ". value=" + getValue()); + for (int i = 0; i < mEntryValues.length; i++) { + Log.v(TAG, "entryValues[" + i + "]=" + mEntryValues[i]); + } + } +} diff --git a/src/com/android/camera/LocationManager.java b/src/com/android/camera/LocationManager.java new file mode 100644 index 000000000..fcf21b60f --- /dev/null +++ b/src/com/android/camera/LocationManager.java @@ -0,0 +1,181 @@ +/* + * 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.location.Location; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; + +/** + * A class that handles everything about location. + */ +public class LocationManager { + private static final String TAG = "LocationManager"; + + private Context mContext; + private Listener mListener; + private android.location.LocationManager mLocationManager; + private boolean mRecordLocation; + + LocationListener [] mLocationListeners = new LocationListener[] { + new LocationListener(android.location.LocationManager.GPS_PROVIDER), + new LocationListener(android.location.LocationManager.NETWORK_PROVIDER) + }; + + public interface Listener { + public void showGpsOnScreenIndicator(boolean hasSignal); + public void hideGpsOnScreenIndicator(); + } + + public LocationManager(Context context, Listener listener) { + mContext = context; + mListener = listener; + } + + public Location getCurrentLocation() { + if (!mRecordLocation) return null; + + // go in best to worst order + for (int i = 0; i < mLocationListeners.length; i++) { + Location l = mLocationListeners[i].current(); + if (l != null) return l; + } + Log.d(TAG, "No location received yet."); + return null; + } + + public void recordLocation(boolean recordLocation) { + if (mRecordLocation != recordLocation) { + mRecordLocation = recordLocation; + if (recordLocation) { + startReceivingLocationUpdates(); + } else { + stopReceivingLocationUpdates(); + } + } + } + + private void startReceivingLocationUpdates() { + if (mLocationManager == null) { + mLocationManager = (android.location.LocationManager) + mContext.getSystemService(Context.LOCATION_SERVICE); + } + if (mLocationManager != null) { + try { + mLocationManager.requestLocationUpdates( + android.location.LocationManager.NETWORK_PROVIDER, + 1000, + 0F, + mLocationListeners[1]); + } catch (SecurityException ex) { + Log.i(TAG, "fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + Log.d(TAG, "provider does not exist " + ex.getMessage()); + } + try { + mLocationManager.requestLocationUpdates( + android.location.LocationManager.GPS_PROVIDER, + 1000, + 0F, + mLocationListeners[0]); + if (mListener != null) mListener.showGpsOnScreenIndicator(false); + } catch (SecurityException ex) { + Log.i(TAG, "fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + Log.d(TAG, "provider does not exist " + ex.getMessage()); + } + Log.d(TAG, "startReceivingLocationUpdates"); + } + } + + private void stopReceivingLocationUpdates() { + if (mLocationManager != null) { + for (int i = 0; i < mLocationListeners.length; i++) { + try { + mLocationManager.removeUpdates(mLocationListeners[i]); + } catch (Exception ex) { + Log.i(TAG, "fail to remove location listners, ignore", ex); + } + } + Log.d(TAG, "stopReceivingLocationUpdates"); + } + if (mListener != null) mListener.hideGpsOnScreenIndicator(); + } + + private class LocationListener + implements android.location.LocationListener { + Location mLastLocation; + boolean mValid = false; + String mProvider; + + public LocationListener(String provider) { + mProvider = provider; + mLastLocation = new Location(mProvider); + } + + @Override + public void onLocationChanged(Location newLocation) { + if (newLocation.getLatitude() == 0.0 + && newLocation.getLongitude() == 0.0) { + // Hack to filter out 0.0,0.0 locations + return; + } + // If GPS is available before start camera, we won't get status + // update so update GPS indicator when we receive data. + if (mListener != null && mRecordLocation && + android.location.LocationManager.GPS_PROVIDER.equals(mProvider)) { + mListener.showGpsOnScreenIndicator(true); + } + if (!mValid) { + Log.d(TAG, "Got first location."); + } + mLastLocation.set(newLocation); + mValid = true; + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + mValid = false; + } + + @Override + public void onStatusChanged( + String provider, int status, Bundle extras) { + switch(status) { + case LocationProvider.OUT_OF_SERVICE: + case LocationProvider.TEMPORARILY_UNAVAILABLE: { + mValid = false; + if (mListener != null && mRecordLocation && + android.location.LocationManager.GPS_PROVIDER.equals(provider)) { + mListener.showGpsOnScreenIndicator(false); + } + break; + } + } + } + + public Location current() { + return mValid ? mLastLocation : null; + } + } +} diff --git a/src/com/android/camera/MediaSaver.java b/src/com/android/camera/MediaSaver.java new file mode 100644 index 000000000..a3d582e1c --- /dev/null +++ b/src/com/android/camera/MediaSaver.java @@ -0,0 +1,149 @@ +/* + * 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.location.Location; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; + +// We use a queue to store the SaveRequests that have not been completed +// yet. The main thread puts the request into the queue. The saver thread +// gets it from the queue, does the work, and removes it from the queue. +// +// The main thread needs to wait for the saver thread to finish all the work +// in the queue, when the activity's onPause() is called, we need to finish +// all the work, so other programs (like Gallery) can see all the images. +// +// If the queue becomes too long, adding a new request will block the main +// thread until the queue length drops below the threshold (QUEUE_LIMIT). +// If we don't do this, we may face several problems: (1) We may OOM +// because we are holding all the jpeg data in memory. (2) We may ANR +// when we need to wait for saver thread finishing all the work (in +// onPause() or gotoGallery()) because the time to finishing a long queue +// of work may be too long. +class MediaSaver extends Thread { + private static final int SAVE_QUEUE_LIMIT = 3; + private static final String TAG = "MediaSaver"; + + private ArrayList mQueue; + private boolean mStop; + private ContentResolver mContentResolver; + + public interface OnMediaSavedListener { + public void onMediaSaved(Uri uri); + } + + public MediaSaver(ContentResolver resolver) { + mContentResolver = resolver; + mQueue = new ArrayList(); + start(); + } + + // Runs in main thread + public synchronized boolean queueFull() { + return (mQueue.size() >= SAVE_QUEUE_LIMIT); + } + + // Runs in main thread + public void addImage(final byte[] data, String title, long date, Location loc, + int width, int height, int orientation, OnMediaSavedListener l) { + SaveRequest r = new SaveRequest(); + r.data = data; + r.date = date; + r.title = title; + r.loc = (loc == null) ? null : new Location(loc); // make a copy + r.width = width; + r.height = height; + r.orientation = orientation; + r.listener = l; + synchronized (this) { + while (mQueue.size() >= SAVE_QUEUE_LIMIT) { + try { + wait(); + } catch (InterruptedException ex) { + // ignore. + } + } + mQueue.add(r); + notifyAll(); // Tell saver thread there is new work to do. + } + } + + // Runs in saver thread + @Override + public void run() { + while (true) { + SaveRequest r; + synchronized (this) { + if (mQueue.isEmpty()) { + notifyAll(); // notify main thread in waitDone + + // Note that we can only stop after we saved all images + // in the queue. + if (mStop) break; + + try { + wait(); + } catch (InterruptedException ex) { + // ignore. + } + continue; + } + if (mStop) break; + r = mQueue.remove(0); + notifyAll(); // the main thread may wait in addImage + } + Uri uri = storeImage(r.data, r.title, r.date, r.loc, r.width, r.height, + r.orientation); + r.listener.onMediaSaved(uri); + } + if (!mQueue.isEmpty()) { + Log.e(TAG, "Media saver thread stopped with " + mQueue.size() + " images unsaved"); + mQueue.clear(); + } + } + + // Runs in main thread + public void finish() { + synchronized (this) { + mStop = true; + notifyAll(); + } + } + + // Runs in saver thread + private Uri storeImage(final byte[] data, String title, long date, + Location loc, int width, int height, int orientation) { + Uri uri = Storage.addImage(mContentResolver, title, date, loc, + orientation, data, width, height); + return uri; + } + + // Each SaveRequest remembers the data needed to save an image. + private static class SaveRequest { + byte[] data; + String title; + long date; + Location loc; + int width, height; + int orientation; + OnMediaSavedListener listener; + } +} diff --git a/src/com/android/camera/Mosaic.java b/src/com/android/camera/Mosaic.java new file mode 100644 index 000000000..78876c384 --- /dev/null +++ b/src/com/android/camera/Mosaic.java @@ -0,0 +1,206 @@ +/* + * 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 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..c59e6b91b --- /dev/null +++ b/src/com/android/camera/MosaicFrameProcessor.java @@ -0,0 +1,236 @@ +/* + * 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; + +/** + * Class to handle the processing of each frame by Mosaicer. + */ +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); + reset(); + } + + 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..e12fe432e --- /dev/null +++ b/src/com/android/camera/MosaicPreviewRenderer.java @@ -0,0 +1,264 @@ +/* + * 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.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 android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class MosaicPreviewRenderer { + private static final String TAG = "MosaicPreviewRenderer"; + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final boolean DEBUG = false; + + 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 EGLHandler mEglHandler; + + private EGLConfig mEglConfig; + private EGLDisplay mEglDisplay; + private EGLContext mEglContext; + private EGLSurface mEglSurface; + private SurfaceTexture mMosaicOutputSurfaceTexture; + private SurfaceTexture mInputSurfaceTexture; + private EGL10 mEgl; + private GL10 mGl; + + private class EGLHandler extends Handler { + public static final int MSG_INIT_EGL_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 EGLHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT_EGL_SYNC: + doInitGL(); + 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(); + 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(); + draw(); + mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); + } + + 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(); + draw(); + mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); + } + + private void doInitGL() { + // These are copied from GLSurfaceView + mEgl = (EGL10) EGLContext.getEGL(); + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } else { + Log.v(TAG, "EGL version: " + version[0] + '.' + version[1]); + } + int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + mEglConfig = chooseConfig(mEgl, mEglDisplay); + mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, + attribList); + + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + throw new RuntimeException("failed to createContext"); + } + mEglSurface = mEgl.eglCreateWindowSurface( + mEglDisplay, mEglConfig, mMosaicOutputSurfaceTexture, null); + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + throw new RuntimeException("failed to createWindowSurface"); + } + + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + throw new RuntimeException("failed to eglMakeCurrent"); + } + + mGl = (GL10) mEglContext.getGL(); + + mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init()); + MosaicRenderer.reset(mWidth, mHeight, mIsLandscape); + } + + private void doRelease() { + mEgl.eglDestroySurface(mEglDisplay, mEglSurface); + mEgl.eglDestroyContext(mEglDisplay, mEglContext); + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + mEgl.eglTerminate(mEglDisplay); + mEglSurface = null; + mEglContext = null; + mEglDisplay = null; + 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(); + } + + } + + public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape) { + mMosaicOutputSurfaceTexture = tex; + mWidth = w; + mHeight = h; + mIsLandscape = isLandscape; + + mEglThread = new HandlerThread("PanoramaRealtimeRenderer"); + mEglThread.start(); + mEglHandler = new EGLHandler(mEglThread.getLooper()); + + // 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. + mEglHandler.sendMessageSync(EGLHandler.MSG_INIT_EGL_SYNC); + } + + public void release() { + mEglHandler.sendEmptyMessage(EGLHandler.MSG_RELEASE); + } + + public void showPreviewFrameSync() { + mEglHandler.sendMessageSync(EGLHandler.MSG_SHOW_PREVIEW_FRAME_SYNC); + } + + public void showPreviewFrame() { + mEglHandler.sendEmptyMessage(EGLHandler.MSG_SHOW_PREVIEW_FRAME); + } + + public void alignFrameSync() { + mEglHandler.sendMessageSync(EGLHandler.MSG_ALIGN_FRAME_SYNC); + } + + public SurfaceTexture getInputSurfaceTexture() { + return mInputSurfaceTexture; + } + + private void draw() { + MosaicRenderer.step(); + } + + private static void checkEglError(String prompt, EGL10 egl) { + int error; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error)); + } + } + + private static final int EGL_OPENGL_ES2_BIT = 4; + private static final int[] CONFIG_SPEC = new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_NONE + }; + + private static EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + if (!egl.eglChooseConfig(display, CONFIG_SPEC, null, 0, numConfig)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = numConfig[0]; + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig( + display, CONFIG_SPEC, configs, numConfigs, numConfig)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + + return configs[0]; + } +} 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/OnClickAttr.java b/src/com/android/camera/OnClickAttr.java new file mode 100644 index 000000000..07a10635b --- /dev/null +++ b/src/com/android/camera/OnClickAttr.java @@ -0,0 +1,31 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Interface for OnClickAttr annotation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnClickAttr { +} diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java new file mode 100644 index 000000000..80063e429 --- /dev/null +++ b/src/com/android/camera/OnScreenHint.java @@ -0,0 +1,188 @@ +/* + * 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.content.Context; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +/** + * A on-screen hint is a view containing a little message for the user and will + * be shown on the screen continuously. This class helps you create and show + * those. + * + *

+ * When the view is shown to the user, appears as a floating view over the + * application. + *

+ * The easiest way to use this class is to call one of the static methods that + * constructs everything you need and returns a new {@code OnScreenHint} object. + */ +public class OnScreenHint { + static final String TAG = "OnScreenHint"; + + int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + int mX, mY; + float mHorizontalMargin; + float mVerticalMargin; + View mView; + View mNextView; + + private final WindowManager.LayoutParams mParams = + new WindowManager.LayoutParams(); + private final WindowManager mWM; + private final Handler mHandler = new Handler(); + + /** + * Construct an empty OnScreenHint object. + * + * @param context The context to use. Usually your + * {@link android.app.Application} or + * {@link android.app.Activity} object. + */ + private OnScreenHint(Context context) { + mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mY = context.getResources().getDimensionPixelSize( + R.dimen.hint_y_offset); + + mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + mParams.format = PixelFormat.TRANSLUCENT; + mParams.windowAnimations = R.style.Animation_OnScreenHint; + mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + mParams.setTitle("OnScreenHint"); + } + + /** + * Show the view on the screen. + */ + public void show() { + if (mNextView == null) { + throw new RuntimeException("View is not initialized"); + } + mHandler.post(mShow); + } + + /** + * Close the view if it's showing. + */ + public void cancel() { + mHandler.post(mHide); + } + + /** + * Make a standard hint that just contains a text view. + * + * @param context The context to use. Usually your + * {@link android.app.Application} or + * {@link android.app.Activity} object. + * @param text The text to show. Can be formatted text. + * + */ + public static OnScreenHint makeText(Context context, CharSequence text) { + OnScreenHint result = new OnScreenHint(context); + + LayoutInflater inflate = + (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View v = inflate.inflate(R.layout.on_screen_hint, null); + TextView tv = (TextView) v.findViewById(R.id.message); + tv.setText(text); + + result.mNextView = v; + + return result; + } + + /** + * Update the text in a OnScreenHint that was previously created using one + * of the makeText() methods. + * @param s The new text for the OnScreenHint. + */ + public void setText(CharSequence s) { + if (mNextView == null) { + throw new RuntimeException("This OnScreenHint was not " + + "created with OnScreenHint.makeText()"); + } + TextView tv = (TextView) mNextView.findViewById(R.id.message); + if (tv == null) { + throw new RuntimeException("This OnScreenHint was not " + + "created with OnScreenHint.makeText()"); + } + tv.setText(s); + } + + private synchronized void handleShow() { + if (mView != mNextView) { + // remove the old view if necessary + handleHide(); + mView = mNextView; + final int gravity = mGravity; + mParams.gravity = gravity; + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) + == Gravity.FILL_HORIZONTAL) { + mParams.horizontalWeight = 1.0f; + } + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) + == Gravity.FILL_VERTICAL) { + mParams.verticalWeight = 1.0f; + } + mParams.x = mX; + mParams.y = mY; + mParams.verticalMargin = mVerticalMargin; + mParams.horizontalMargin = mHorizontalMargin; + if (mView.getParent() != null) { + mWM.removeView(mView); + } + mWM.addView(mView, mParams); + } + } + + private synchronized void handleHide() { + if (mView != null) { + // note: checking parent() just to make sure the view has + // been added... i have seen cases where we get here when + // the view isn't yet added, so let's try not to crash. + if (mView.getParent() != null) { + mWM.removeView(mView); + } + mView = null; + } + } + + private final Runnable mShow = new Runnable() { + @Override + public void run() { + handleShow(); + } + }; + + private final Runnable mHide = new Runnable() { + @Override + public void run() { + handleHide(); + } + }; +} + 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/PanoramaModule.java b/src/com/android/camera/PanoramaModule.java new file mode 100644 index 000000000..18290872e --- /dev/null +++ b/src/com/android/camera/PanoramaModule.java @@ -0,0 +1,1312 @@ +/* + * 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.annotation.TargetApi; +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.Canvas; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.graphics.YuvImage; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +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.LayoutInflater; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.LayoutChangeNotifier; +import com.android.camera.ui.LayoutNotifyView; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.Rotatable; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifData; +import com.android.gallery3d.exif.ExifInvalidFormatException; +import com.android.gallery3d.exif.ExifOutputStream; +import com.android.gallery3d.exif.ExifReader; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.ui.GLRootView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.TimeZone; + +/** + * Activity to handle panorama capturing. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class PanoramaModule implements CameraModule, + SurfaceTexture.OnFrameAvailableListener, + ShutterButton.OnShutterButtonListener, + LayoutChangeNotifier.Listener { + + 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_CONFIG_MOSAIC_PREVIEW = 5; + private static final int MSG_RESET_TO_PREVIEW = 6; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private static final String TAG = "CAM PanoModule"; + private static final int PREVIEW_STOPPED = 0; + private static final int PREVIEW_ACTIVE = 1; + private static final int CAPTURE_STATE_VIEWFINDER = 0; + private 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 ContentResolver mContentResolver; + + private GLRootView mGLRootView; + private ViewGroup mPanoLayout; + private LinearLayout mCaptureLayout; + private View mReviewLayout; + private ImageView mReview; + private View mCaptureIndicator; + private PanoProgressBar mPanoProgressBar; + private PanoProgressBar mSavingProgressBar; + private Matrix mProgressDirectionMatrix = new Matrix(); + private float[] mProgressAngle = new float[2]; + private LayoutNotifyView mPreviewArea; + private View mLeftIndicator; + private View mRightIndicator; + private MosaicPreviewRenderer mMosaicPreviewRenderer; + private Object mRendererLock = new Object(); + private TextView mTooFastPrompt; + private ShutterButton mShutterButton; + private Object mWaitObject = new Object(); + + private String mPreparePreviewString; + private String mDialogTitle; + private String mDialogOkString; + private String mDialogPanoramaFailedString; + private String mDialogWaitingPreviousString; + + private int mIndicatorColor; + private int mIndicatorColorFast; + private int mReviewBackground; + + private boolean mUsingFrontCamera; + private int mPreviewWidth; + private int mPreviewHeight; + 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 RotateDialogController mRotateDialog; + + private SoundClips.Player mSoundPlayer; + + private Runnable mOnFrameAvailableRunnable; + + private CameraActivity mActivity; + private View mRootView; + private CameraProxy mCameraDevice; + private boolean mPaused; + private boolean mIsCreatingRenderer; + private boolean mIsConfigPending; + + 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 = Util.roundOrientation(orientation, mDeviceOrientation); + // When the screen is unlocked, display rotation may change. Always + // calculate the up-to-date orientationCompensation. + int orientationCompensation = mDeviceOrientation + + Util.getDisplayRotation(mActivity) % 360; + if (mOrientationCompensation != orientationCompensation) { + mOrientationCompensation = orientationCompensation; + mActivity.getGLRoot().requestLayoutContentPane(); + } + } + } + + @Override + public void init(CameraActivity activity, View parent, boolean reuseScreenNail) { + mActivity = activity; + mRootView = parent; + + createContentView(); + + mContentResolver = mActivity.getContentResolver(); + if (reuseScreenNail) { + mActivity.reuseCameraScreenNail(true); + } else { + mActivity.createCameraScreenNail(true); + } + + // 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) { + try { + while (mMosaicPreviewRenderer == null) { + mRendererLock.wait(); + } + renderer = mMosaicPreviewRenderer; + } catch (InterruptedException e) { + Log.e(TAG, "Unexpected interruption", e); + } + } + if (mGLRootView.getVisibility() != View.VISIBLE) { + renderer.showPreviewFrameSync(); + mGLRootView.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); + + mGLRootView = (GLRootView) mActivity.getGLRoot(); + + 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) { + resetToPreview(); + } else { + mRotateDialog.showAlertDialog( + mDialogTitle, mDialogPanoramaFailedString, + mDialogOkString, new Runnable() { + @Override + public void run() { + resetToPreview(); + }}, + null, null); + } + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_END_DIALOG_RESET_TO_PREVIEW: + onBackgroundThreadFinished(); + resetToPreview(); + clearMosaicFrameProcessorIfNeeded(); + break; + case MSG_CLEAR_SCREEN_DELAY: + mActivity.getWindow().clearFlags(WindowManager.LayoutParams. + FLAG_KEEP_SCREEN_ON); + break; + case MSG_CONFIG_MOSAIC_PREVIEW: + configMosaicPreview(msg.arg1, msg.arg2); + break; + case MSG_RESET_TO_PREVIEW: + resetToPreview(); + break; + } + } + }; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + return mActivity.superDispatchTouchEvent(m); + } + + private void setupCamera() throws CameraHardwareException, CameraDisabledException { + openCamera(); + Parameters parameters = mCameraDevice.getParameters(); + setupCaptureParams(parameters); + configureCamera(parameters); + } + + private void releaseCamera() { + if (mCameraDevice != null) { + mCameraDevice.setPreviewCallbackWithBuffer(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 = Util.openCamera(mActivity, cameraId); + mCameraOrientation = Util.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) { + mPreviewWidth = w; + mPreviewHeight = 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.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); + parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); + + 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.v(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(Util.RECORDING_HINT, Util.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 (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; + } + + private void configureCamera(Parameters parameters) { + mCameraDevice.setParameters(parameters); + } + + private void configMosaicPreview(final int w, final int h) { + synchronized (mRendererLock) { + if (mIsCreatingRenderer) { + mMainHandler.removeMessages(MSG_CONFIG_MOSAIC_PREVIEW); + mMainHandler.obtainMessage(MSG_CONFIG_MOSAIC_PREVIEW, w, h).sendToTarget(); + mIsConfigPending = true; + return; + } + mIsCreatingRenderer = true; + mIsConfigPending = false; + } + stopCameraPreview(); + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + screenNail.setSize(w, h); + synchronized (mRendererLock) { + if (mMosaicPreviewRenderer != null) { + mMosaicPreviewRenderer.release(); + } + mMosaicPreviewRenderer = null; + screenNail.releaseSurfaceTexture(); + screenNail.acquireSurfaceTexture(); + } + mActivity.notifyScreenNailChanged(); + final boolean isLandscape = (mActivity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); + new Thread(new Runnable() { + @Override + public void run() { + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + SurfaceTexture surfaceTexture = screenNail.getSurfaceTexture(); + if (surfaceTexture == null) { + synchronized (mRendererLock) { + mIsConfigPending = true; // try config again later. + mIsCreatingRenderer = false; + mRendererLock.notifyAll(); + return; + } + } + MosaicPreviewRenderer renderer = new MosaicPreviewRenderer( + screenNail.getSurfaceTexture(), w, h, isLandscape); + synchronized (mRendererLock) { + mMosaicPreviewRenderer = renderer; + mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); + + if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { + mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); + } + mIsCreatingRenderer = false; + mRendererLock.notifyAll(); + } + } + }).start(); + } + + // Receives the layout change event from the preview area. So we can set + // the camera preview screennail to the same size and initialize the mosaic + // preview renderer. + @Override + public void onLayoutChange(View v, int l, int t, int r, int b) { + Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t)); + mActivity.onLayoutChange(v, l, t, r, b); + configMosaicPreview(r - l, b - t); + } + + @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); + } + + private void hideDirectionIndicators() { + mLeftIndicator.setVisibility(View.GONE); + mRightIndicator.setVisibility(View.GONE); + } + + private 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.GONE); + break; + case PanoProgressBar.DIRECTION_RIGHT: + mLeftIndicator.setVisibility(View.GONE); + mRightIndicator.setVisibility(View.VISIBLE); + break; + } + } + + public void startCapture() { + // Reset values so we can do this again. + mCancelComputation = false; + mTimeTaken = System.currentTimeMillis(); + mActivity.setSwipingEnabled(false); + mActivity.hideSwitcher(); + mShutterButton.setImageResource(R.drawable.btn_shutter_recording); + mCaptureState = CAPTURE_STATE_MOSAIC; + mCaptureIndicator.setVisibility(View.VISIBLE); + showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); + + 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; + updateProgress(panningRateXInDegree, panningRateYInDegree, + accumulatedHorizontalAngle, accumulatedVerticalAngle); + } + } + }); + + mPanoProgressBar.reset(); + // TODO: calculate the indicator width according to different devices to reflect the actual + // angle of view of the camera device. + mPanoProgressBar.setIndicatorWidth(20); + mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); + mPanoProgressBar.setVisibility(View.VISIBLE); + mDeviceOrientationAtCapture = mDeviceOrientation; + keepScreenOn(); + mActivity.getOrientationManager().lockOrientation(); + setupProgressDirectionMatrix(); + } + + void setupProgressDirectionMatrix() { + int degrees = Util.getDisplayRotation(mActivity); + int cameraId = CameraHolder.instance().getBackCameraId(); + int orientation = Util.getDisplayOrientation(degrees, cameraId); + mProgressDirectionMatrix.reset(); + mProgressDirectionMatrix.postRotate(orientation); + } + + private void stopCapture(boolean aborted) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mCaptureIndicator.setVisibility(View.GONE); + hideTooFastIndication(); + hideDirectionIndicators(); + + mMosaicFrameProcessor.setProgressListener(null); + stopCameraPreview(); + + mCameraTexture.setOnFrameAvailableListener(null); + + if (!aborted && !mThreadRunning) { + mRotateDialog.showWaitingDialog(mPreparePreviewString); + // Hide shutter button, shutter icon, etc when waiting for + // panorama to stitch + mActivity.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(); + } + + private void showTooFastIndication() { + mTooFastPrompt.setVisibility(View.VISIBLE); + // The PreviewArea also contains the border for "too fast" indication. + mPreviewArea.setVisibility(View.VISIBLE); + mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); + mLeftIndicator.setEnabled(true); + mRightIndicator.setEnabled(true); + } + + private void hideTooFastIndication() { + mTooFastPrompt.setVisibility(View.GONE); + // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout + // information so we can know the size and position for mCameraScreenNail. + mPreviewArea.setVisibility(View.INVISIBLE); + mPanoProgressBar.setIndicatorColor(mIndicatorColor); + mLeftIndicator.setEnabled(false); + mRightIndicator.setEnabled(false); + } + + private void updateProgress(float panningRateXInDegree, float panningRateYInDegree, + float progressHorizontalAngle, float progressVerticalAngle) { + mGLRootView.requestRender(); + + if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD) + || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) { + 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]; + mPanoProgressBar.setProgress((angleInMajorDirection)); + } + + private void setViews(Resources appRes) { + mCaptureState = CAPTURE_STATE_VIEWFINDER; + mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar); + mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); + mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); + mPanoProgressBar.setIndicatorColor(mIndicatorColor); + mPanoProgressBar.setOnDirectionChangeListener( + new PanoProgressBar.OnDirectionChangeListener () { + @Override + public void onDirectionChange(int direction) { + if (mCaptureState == CAPTURE_STATE_MOSAIC) { + showDirectionIndicators(direction); + } + } + }); + + 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); + // This mPreviewArea also shows the border for visual "too fast" indication. + mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area); + mPreviewArea.setOnLayoutChangeListener(this); + + 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 OnClickListener() { + @Override + public void onClick(View arg0) { + if (mPaused || mCameraTexture == null) return; + cancelHighResComputation(); + } + }); + + mShutterButton = mActivity.getShutterButton(); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(this); + + if (mActivity.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT) { + Rotatable view = (Rotatable) mRootView.findViewById(R.id.pano_rotate_reviewarea); + view.setOrientation(270, false); + } + } + + private void createContentView() { + mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView); + Resources appRes = mActivity.getResources(); + mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root); + mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); + mReviewBackground = appRes.getColor(R.color.review_background); + mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); + mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout); + mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog); + setViews(appRes); + } + + @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); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + } + + public void reportProgress() { + mSavingProgressBar.reset(); + mSavingProgressBar.setRightIncreasing(true); + 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() { + mSavingProgressBar.setProgress(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(); + Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); + if (uri != null) { + mActivity.addSecureAlbumItemIfNeeded(false, uri); + Util.broadcastNewPicture(mActivity, 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; + mRotateDialog.dismissDialog(); + } + + 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; + + mActivity.getOrientationManager().unlockOrientation(); + // We should set mGLRootView visible too. However, since there might be no + // frame available yet, setting mGLRootView visible should be done right after + // the first camera frame is available and therefore it is done by + // mOnFirstFrameAvailableRunnable. + mActivity.setSwipingEnabled(true); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mReviewLayout.setVisibility(View.GONE); + mPanoProgressBar.setVisibility(View.GONE); + mGLRootView.setVisibility(View.VISIBLE); + // Orientation change will trigger onLayoutChange->configMosaicPreview-> + // resetToPreview. Do not show the capture UI in film strip. + if (mActivity.mShowCameraAppView) { + mCaptureLayout.setVisibility(View.VISIBLE); + mActivity.showUI(); + } + mMosaicFrameProcessor.reset(); + } + + private void resetToPreview() { + reset(); + if (!mPaused) startCameraPreview(); + } + + 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(); + } + } + + private void showFinalMosaic(Bitmap bitmap) { + if (bitmap != null) { + int orientation = getCaptureOrientation(); + if (orientation >= 180) { + // We need to flip the drawable to compensate + mReview.setImageDrawable(new FlipBitmapDrawable( + mActivity.getResources(), bitmap)); + } else { + mReview.setImageBitmap(bitmap); + } + } + + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + + 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); + + ExifOutputStream out = null; + InputStream is = null; + try { + is = new ByteArrayInputStream(jpegData); + ExifReader reader = new ExifReader(); + ExifData data = reader.read(is); + + // Add Exif tags. + data.addGpsDateTimeStampTag(mTimeTaken); + data.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault()); + data.addTag(ExifTag.TAG_ORIENTATION). + setValue(getExifOrientation(orientation)); + + out = new ExifOutputStream(new FileOutputStream(filepath)); + out.setExifData(data); + out.write(jpegData); + } catch (IOException e) { + Log.e(TAG, "Cannot set EXIF for " + filepath, e); + Storage.writeFile(filepath, jpegData); + } catch (ExifInvalidFormatException e) { + Log.e(TAG, "Cannot set EXIF for " + filepath, e); + Storage.writeFile(filepath, jpegData); + } finally { + Util.closeSilently(out); + Util.closeSilently(is); + } + + int jpegLength = (int) (new File(filepath).length()); + return Storage.addImage(mContentResolver, filename, mTimeTaken, + null, orientation, jpegLength, filepath, width, height); + } + return null; + } + + private static int getExifOrientation(int orientation) { + switch (orientation) { + case 0: + return ExifTag.Orientation.TOP_LEFT; + case 90: + return ExifTag.Orientation.RIGHT_TOP; + case 180: + return ExifTag.Orientation.BOTTOM_LEFT; + case 270: + return ExifTag.Orientation.RIGHT_BOTTOM; + default: + throw new AssertionError("invalid: " + orientation); + } + } + + 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( + mPreviewWidth, mPreviewHeight, getPreviewBufSize()); + mMosaicFrameProcessorInitialized = true; + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + } + + @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; + } + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + screenNail.releaseSurfaceTexture(); + System.gc(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + + Drawable lowResReview = null; + if (mThreadRunning) lowResReview = mReview.getDrawable(); + + // Change layout in response to configuration change + mCaptureLayout.setOrientation( + newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE + ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + mCaptureLayout.removeAllViews(); + LayoutInflater inflater = mActivity.getLayoutInflater(); + inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout); + + mPanoLayout.removeView(mReviewLayout); + inflater.inflate(R.layout.pano_review, mPanoLayout); + + setViews(mActivity.getResources()); + if (mThreadRunning) { + mReview.setImageDrawable(lowResReview); + mCaptureLayout.setVisibility(View.GONE); + mReviewLayout.setVisibility(View.VISIBLE); + } + } + + @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) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + Util.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. + mRotateDialog.dismissDialog(); + if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { + mGLRootView.setVisibility(View.GONE); + mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString); + // If stitching is still going on, make sure switcher and shutter button + // are not showing + mActivity.hideUI(); + mWaitProcessorTask = new WaitProcessorTask().execute(); + } else { + mGLRootView.setVisibility(View.VISIBLE); + // Camera must be initialized before MosaicFrameProcessor is + // initialized. The preview size has to be decided by camera device. + initMosaicFrameProcessorIfNeeded(); + int w = mPreviewArea.getWidth(); + int h = mPreviewArea.getHeight(); + if (w != 0 && h != 0) { // The layout has been calculated. + configMosaicPreview(w, h); + } + } + keepScreenOnAwhile(); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + mRootView.requestLayout(); + } + + /** + * 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.v(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; + } + + // 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 mPreviewWidth x mPreviewHeight + // image data from SurfaceTexture. + mCameraDevice.setDisplayOrientation(0); + + mCameraTexture.setOnFrameAvailableListener(this); + mCameraDevice.setPreviewTextureAsync(mCameraTexture); + } + mCameraDevice.startPreviewAsync(); + mCameraState = PREVIEW_ACTIVE; + } + + private void stopCameraPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + Log.v(TAG, "stopPreview"); + 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; + mRotateDialog.dismissDialog(); + mGLRootView.setVisibility(View.VISIBLE); + initMosaicFrameProcessorIfNeeded(); + int w = mPreviewArea.getWidth(); + int h = mPreviewArea.getHeight(); + if (w != 0 && h != 0) { // The layout has been calculated. + configMosaicPreview(w, h); + } + resetToPreview(); + } + } + + @Override + public void onFullScreenChanged(boolean full) { + } + + + @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 boolean collapseCameraControls() { + return false; + } + + @Override + public boolean needsSwitcher() { + return true; + } + + @Override + public void onShowSwitcherPopup() { + } +} diff --git a/src/com/android/camera/PhotoController.java b/src/com/android/camera/PhotoController.java new file mode 100644 index 000000000..ad8659ee8 --- /dev/null +++ b/src/com/android/camera/PhotoController.java @@ -0,0 +1,225 @@ +/* + * 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; + +import android.content.Context; +import android.hardware.Camera.Parameters; +import android.view.LayoutInflater; + +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.ListPrefSettingPopup; +import com.android.camera.ui.MoreSettingPopup; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.TimerSettingPopup; + +public class PhotoController extends PieController + implements MoreSettingPopup.Listener, + TimerSettingPopup.Listener, + ListPrefSettingPopup.Listener { + private static String TAG = "CAM_photocontrol"; + private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2; + private final String mSettingOff; + + private PhotoModule mModule; + private String[] mOtherKeys; + // First level popup + private MoreSettingPopup mPopup; + // Second level popup + private AbstractSettingPopup mSecondPopup; + + public PhotoController(CameraActivity activity, PhotoModule module, PieRenderer pie) { + super(activity, pie); + mModule = module; + mSettingOff = activity.getString(R.string.setting_off_value); + } + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + mSecondPopup = null; + float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2; + addItem(CameraSettings.KEY_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep); + addItem(CameraSettings.KEY_EXPOSURE, 3 * FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep); + addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep); + if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) { + PieItem item = makeItem(R.drawable.ic_switch_photo_facing_holo_light); + item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + // Find the index of next camera. + ListPreference camPref = mPreferenceGroup + .findPreference(CameraSettings.KEY_CAMERA_ID); + if (camPref != null) { + int index = camPref.findIndexOfValue(camPref.getValue()); + CharSequence[] values = camPref.getEntryValues(); + index = (index + 1) % values.length; + int newCameraId = Integer + .parseInt((String) values[index]); + mListener.onCameraPickerClicked(newCameraId); + } + } + }); + mRenderer.addItem(item); + } + if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) { + PieItem hdr = makeItem(R.drawable.ic_hdr); + hdr.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO, sweep); + hdr.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + // Find the index of next camera. + ListPreference pref = mPreferenceGroup + .findPreference(CameraSettings.KEY_CAMERA_HDR); + if (pref != null) { + // toggle hdr value + int index = (pref.findIndexOfValue(pref.getValue()) + 1) % 2; + pref.setValueIndex(index); + onSettingChanged(pref); + } + } + }); + mRenderer.addItem(hdr); + } + mOtherKeys = new String[] { + CameraSettings.KEY_SCENE_MODE, + CameraSettings.KEY_RECORD_LOCATION, + CameraSettings.KEY_PICTURE_SIZE, + CameraSettings.KEY_FOCUS_MODE, + CameraSettings.KEY_TIMER, + CameraSettings.KEY_TIMER_SOUND_EFFECTS, + }; + PieItem item = makeItem(R.drawable.ic_settings_holo_light); + item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + if (mPopup == null) { + initializePopup(); + } + mModule.showPopup(mPopup); + } + }); + mRenderer.addItem(item); + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + @Override + public void reloadPreferences() { + super.reloadPreferences(); + if (mPopup != null) { + mPopup.reloadPreference(); + } + } + + @Override + // Hit when an item in the second-level popup gets selected + public void onListPrefChanged(ListPreference pref) { + if (mPopup != null && mSecondPopup != null) { + mModule.dismissPopup(true); + mPopup.reloadPreference(); + } + onSettingChanged(pref); + } + + @Override + public void overrideSettings(final String ... keyvalues) { + super.overrideSettings(keyvalues); + if (mPopup == null) initializePopup(); + mPopup.overrideSettings(keyvalues); + } + + protected void initializePopup() { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate( + R.layout.more_setting_popup, null, false); + popup.setSettingChangedListener(this); + popup.initialize(mPreferenceGroup, mOtherKeys); + if (mActivity.isSecureCamera()) { + // Prevent location preference from getting changed in secure camera mode + popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false); + } + mPopup = popup; + } + + public void popupDismissed(boolean topPopupOnly) { + // if the 2nd level popup gets dismissed + if (mSecondPopup != null) { + mSecondPopup = null; + if (topPopupOnly) mModule.showPopup(mPopup); + } + } + + // Return true if the preference has the specified key but not the value. + private static boolean notSame(ListPreference pref, String key, String value) { + return (key.equals(pref.getKey()) && !value.equals(pref.getValue())); + } + + private void setPreference(String key, String value) { + ListPreference pref = mPreferenceGroup.findPreference(key); + if (pref != null && !value.equals(pref.getValue())) { + pref.setValue(value); + reloadPreferences(); + } + } + + @Override + public void onSettingChanged(ListPreference pref) { + // Reset the scene mode if HDR is set to on. Reset HDR if scene mode is + // set to non-auto. + if (notSame(pref, CameraSettings.KEY_CAMERA_HDR, mSettingOff)) { + setPreference(CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO); + } else if (notSame(pref, CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO)) { + setPreference(CameraSettings.KEY_CAMERA_HDR, mSettingOff); + } + super.onSettingChanged(pref); + } + + @Override + // Hit when an item in the first-level popup gets selected, then bring up + // the second-level popup + public void onPreferenceClicked(ListPreference pref) { + if (mSecondPopup != null) return; + + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + if (CameraSettings.KEY_TIMER.equals(pref.getKey())) { + TimerSettingPopup timerPopup = (TimerSettingPopup) inflater.inflate( + R.layout.timer_setting_popup, null, false); + timerPopup.initialize(pref); + timerPopup.setSettingChangedListener(this); + mModule.dismissPopup(true); + mSecondPopup = timerPopup; + } else { + ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate( + R.layout.list_pref_setting_popup, null, false); + basic.initialize(pref); + basic.setSettingChangedListener(this); + mModule.dismissPopup(true); + mSecondPopup = basic; + } + mModule.showPopup(mSecondPopup); + } +} diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java new file mode 100644 index 000000000..a283e5949 --- /dev/null +++ b/src/com/android/camera/PhotoModule.java @@ -0,0 +1,2481 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences.Editor; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Face; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.Size; +import android.location.Location; +import android.media.CameraProfile; +import android.net.Uri; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CountDownView; +import com.android.camera.ui.FaceView; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.PreviewSurfaceView; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.Rotatable; +import com.android.camera.ui.RotateTextToast; +import com.android.camera.ui.TwoStateImageView; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.FilterShowActivity; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Formatter; +import java.util.List; + +public class PhotoModule + implements CameraModule, + FocusOverlayManager.Listener, + CameraPreference.OnPreferenceChangedListener, + LocationManager.Listener, + PreviewFrameLayout.OnSizeChangedListener, + ShutterButton.OnShutterButtonListener, + SurfaceHolder.Callback, + PieRenderer.PieListener, + CountDownView.OnCountDownFinishedListener { + + private static final String TAG = "CAM_PhotoModule"; + + // We number the request code from 1000 to avoid collision with Gallery. + private static final int REQUEST_CROP = 1000; + + private static final int SETUP_PREVIEW = 1; + private static final int FIRST_TIME_INIT = 2; + private static final int CLEAR_SCREEN_DELAY = 3; + private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 4; + private static final int CHECK_DISPLAY_ROTATION = 5; + private static final int SHOW_TAP_TO_FOCUS_TOAST = 6; + private static final int SWITCH_CAMERA = 7; + private static final int SWITCH_CAMERA_START_ANIMATION = 8; + private static final int CAMERA_OPEN_DONE = 9; + private static final int START_PREVIEW_DONE = 10; + private static final int OPEN_CAMERA_FAIL = 11; + private static final int CAMERA_DISABLED = 12; + private static final int UPDATE_SECURE_ALBUM_ITEM = 13; + + // The subset of parameters we need to update in setCameraParameters(). + private static final int UPDATE_PARAM_INITIALIZE = 1; + private static final int UPDATE_PARAM_ZOOM = 2; + private static final int UPDATE_PARAM_PREFERENCE = 4; + private static final int UPDATE_PARAM_ALL = -1; + + // This is the timeout to keep the camera in onPause for the first time + // after screen on if the activity is started from secure lock screen. + private static final int KEEP_CAMERA_TIMEOUT = 1000; // ms + + // copied from Camera hierarchy + private CameraActivity mActivity; + private View mRootView; + private CameraProxy mCameraDevice; + private int mCameraId; + private Parameters mParameters; + private boolean mPaused; + private AbstractSettingPopup mPopup; + + // these are only used by Camera + + // The activity is going to switch to the specified camera id. This is + // needed because texture copy is done in GL thread. -1 means camera is not + // switching. + protected int mPendingSwitchCameraId = -1; + private boolean mOpenCameraFail; + private boolean mCameraDisabled; + + // When setCameraParametersWhenIdle() is called, we accumulate the subsets + // needed to be updated in mUpdateSet. + private int mUpdateSet; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private int mZoomValue; // The current zoom value. + private int mZoomMax; + private List mZoomRatios; + + private Parameters mInitialParams; + private boolean mFocusAreaSupported; + private boolean mMeteringAreaSupported; + private boolean mAeLockSupported; + private boolean mAwbLockSupported; + private boolean mContinousFocusSupported; + + // The degrees of the device rotated clockwise from its natural orientation. + private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + private ComboPreferences mPreferences; + + private static final String sTempCropFilename = "crop-temp"; + + private ContentProviderClient mMediaProviderClient; + private ShutterButton mShutterButton; + private boolean mFaceDetectionStarted = false; + + private PreviewFrameLayout mPreviewFrameLayout; + private Object mSurfaceTexture; + private CountDownView mCountDownView; + + // for API level 10 + private PreviewSurfaceView mPreviewSurfaceView; + private volatile SurfaceHolder mCameraSurfaceHolder; + + private FaceView mFaceView; + private RenderOverlay mRenderOverlay; + private Rotatable mReviewCancelButton; + private Rotatable mReviewDoneButton; + private View mReviewRetakeButton; + + // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true. + private String mCropValue; + private Uri mSaveUri; + + private View mMenu; + private View mBlocker; + + // Small indicators which show the camera settings in the viewfinder. + private ImageView mExposureIndicator; + private ImageView mFlashIndicator; + private ImageView mSceneIndicator; + private ImageView mHdrIndicator; + // A view group that contains all the small indicators. + private View mOnScreenIndicators; + + // We use a thread in MediaSaver to do the work of saving images. This + // reduces the shot-to-shot time. + private MediaSaver mMediaSaver; + // Similarly, we use a thread to generate the name of the picture and insert + // it into MediaStore while picture taking is still in progress. + private NamedImages mNamedImages; + + private Runnable mDoSnapRunnable = new Runnable() { + @Override + public void run() { + onShutterButtonClick(); + } + }; + + private final StringBuilder mBuilder = new StringBuilder(); + private final Formatter mFormatter = new Formatter(mBuilder); + private final Object[] mFormatterArgs = new Object[1]; + + /** + * An unpublished intent flag requesting to return as soon as capturing + * is completed. + * + * TODO: consider publishing by moving into MediaStore. + */ + private static final String EXTRA_QUICK_CAPTURE = + "android.intent.extra.quickCapture"; + + // The display rotation in degrees. This is only valid when mCameraState is + // not PREVIEW_STOPPED. + private int mDisplayRotation; + // The value for android.hardware.Camera.setDisplayOrientation. + private int mCameraDisplayOrientation; + // The value for UI components like indicators. + private int mDisplayOrientation; + // The value for android.hardware.Camera.Parameters.setRotation. + private int mJpegRotation; + private boolean mFirstTimeInitialized; + private boolean mIsImageCaptureIntent; + + private static final int PREVIEW_STOPPED = 0; + private static final int IDLE = 1; // preview is active + // Focus is in progress. The exact focus state is in Focus.java. + private static final int FOCUSING = 2; + private static final int SNAPSHOT_IN_PROGRESS = 3; + // Switching between cameras. + private static final int SWITCHING_CAMERA = 4; + private int mCameraState = PREVIEW_STOPPED; + private boolean mSnapshotOnIdle = false; + + private ContentResolver mContentResolver; + + private LocationManager mLocationManager; + + private final ShutterCallback mShutterCallback = new ShutterCallback(); + private final PostViewPictureCallback mPostViewPictureCallback = + new PostViewPictureCallback(); + private final RawPictureCallback mRawPictureCallback = + new RawPictureCallback(); + private final AutoFocusCallback mAutoFocusCallback = + new AutoFocusCallback(); + private final Object mAutoFocusMoveCallback = + ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK + ? new AutoFocusMoveCallback() + : null; + + private final CameraErrorCallback mErrorCallback = new CameraErrorCallback(); + + private long mFocusStartTime; + private long mShutterCallbackTime; + private long mPostViewPictureCallbackTime; + private long mRawPictureCallbackTime; + private long mJpegPictureCallbackTime; + private long mOnResumeTime; + private byte[] mJpegImageData; + + // These latency time are for the CameraLatency test. + public long mAutoFocusTime; + public long mShutterLag; + public long mShutterToPictureDisplayedTime; + public long mPictureDisplayedToJpegCallbackTime; + public long mJpegCallbackFinishTime; + public long mCaptureStartTime; + + // This handles everything about focus. + private FocusOverlayManager mFocusManager; + + private PieRenderer mPieRenderer; + private PhotoController mPhotoControl; + + private ZoomRenderer mZoomRenderer; + + private String mSceneMode; + private Toast mNotSelectableToast; + + private final Handler mHandler = new MainHandler(); + private PreferenceGroup mPreferenceGroup; + + private boolean mQuickCapture; + + CameraStartUpThread mCameraStartUpThread; + ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable(); + + private PreviewGestures mGestures; + + private MediaSaver.OnMediaSavedListener mOnMediaSavedListener = new MediaSaver.OnMediaSavedListener() { + @Override + + public void onMediaSaved(Uri uri) { + if (uri != null) { + mHandler.obtainMessage(UPDATE_SECURE_ALBUM_ITEM, uri).sendToTarget(); + Util.broadcastNewPicture(mActivity, uri); + } + } + }; + + // The purpose is not to block the main thread in onCreate and onResume. + private class CameraStartUpThread extends Thread { + private volatile boolean mCancelled; + + public void cancel() { + mCancelled = true; + interrupt(); + } + + public boolean isCanceled() { + return mCancelled; + } + + @Override + public void run() { + try { + // We need to check whether the activity is paused before long + // operations to ensure that onPause() can be done ASAP. + if (mCancelled) return; + mCameraDevice = Util.openCamera(mActivity, mCameraId); + mParameters = mCameraDevice.getParameters(); + // Wait until all the initialization needed by startPreview are + // done. + mStartPreviewPrerequisiteReady.block(); + + initializeCapabilities(); + if (mFocusManager == null) initializeFocusManager(); + if (mCancelled) return; + setCameraParameters(UPDATE_PARAM_ALL); + mHandler.sendEmptyMessage(CAMERA_OPEN_DONE); + if (mCancelled) return; + startPreview(); + mHandler.sendEmptyMessage(START_PREVIEW_DONE); + mOnResumeTime = SystemClock.uptimeMillis(); + mHandler.sendEmptyMessage(CHECK_DISPLAY_ROTATION); + } catch (CameraHardwareException e) { + mHandler.sendEmptyMessage(OPEN_CAMERA_FAIL); + } catch (CameraDisabledException e) { + mHandler.sendEmptyMessage(CAMERA_DISABLED); + } + } + } + + /** + * This Handler is used to post message back onto the main thread of the + * application + */ + private class MainHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SETUP_PREVIEW: { + setupPreview(); + break; + } + + case CLEAR_SCREEN_DELAY: { + mActivity.getWindow().clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + break; + } + + case FIRST_TIME_INIT: { + initializeFirstTime(); + break; + } + + case SET_CAMERA_PARAMETERS_WHEN_IDLE: { + setCameraParametersWhenIdle(0); + break; + } + + case CHECK_DISPLAY_ROTATION: { + // Set the display orientation if display rotation has changed. + // Sometimes this happens when the device is held upside + // down and camera app is opened. Rotation animation will + // take some time and the rotation value we have got may be + // wrong. Framework does not have a callback for this now. + if (Util.getDisplayRotation(mActivity) != mDisplayRotation) { + setDisplayOrientation(); + } + if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) { + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + break; + } + + case SHOW_TAP_TO_FOCUS_TOAST: { + showTapToFocusToast(); + break; + } + + case SWITCH_CAMERA: { + switchCamera(); + break; + } + + case SWITCH_CAMERA_START_ANIMATION: { + ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + break; + } + + case CAMERA_OPEN_DONE: { + initializeAfterCameraOpen(); + break; + } + + case START_PREVIEW_DONE: { + mCameraStartUpThread = null; + setCameraState(IDLE); + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + // This may happen if surfaceCreated has arrived. + mCameraDevice.setPreviewDisplayAsync(mCameraSurfaceHolder); + } + startFaceDetection(); + locationFirstRun(); + break; + } + + case OPEN_CAMERA_FAIL: { + mCameraStartUpThread = null; + mOpenCameraFail = true; + Util.showErrorAndFinish(mActivity, + R.string.cannot_connect_camera); + break; + } + + case CAMERA_DISABLED: { + mCameraStartUpThread = null; + mCameraDisabled = true; + Util.showErrorAndFinish(mActivity, + R.string.camera_disabled); + break; + } + + case UPDATE_SECURE_ALBUM_ITEM: { + mActivity.addSecureAlbumItemIfNeeded(false, (Uri) msg.obj); + break; + } + } + } + } + + @Override + public void init(CameraActivity activity, View parent, boolean reuseNail) { + mActivity = activity; + mRootView = parent; + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mCameraId = getPreferredCameraId(mPreferences); + + mContentResolver = mActivity.getContentResolver(); + + // To reduce startup time, open the camera and start the preview in + // another thread. + mCameraStartUpThread = new CameraStartUpThread(); + mCameraStartUpThread.start(); + + mActivity.getLayoutInflater().inflate(R.layout.photo_module, (ViewGroup) mRootView); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsImageCaptureIntent = isImageCaptureIntent(); + if (reuseNail) { + mActivity.reuseCameraScreenNail(!mIsImageCaptureIntent); + } else { + mActivity.createCameraScreenNail(!mIsImageCaptureIntent); + } + + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + // we need to reset exposure for the preview + resetExposureCompensation(); + // Starting the preview needs preferences, camera screen nail, and + // focus area indicator. + mStartPreviewPrerequisiteReady.open(); + + initializeControlByIntent(); + mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); + initializeMiscControls(); + mLocationManager = new LocationManager(mActivity, this); + initOnScreenIndicator(); + mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture)); + mCountDownView.setCountDownFinishedListener(this); + } + + // Prompt the user to pick to record location for the very first run of + // camera only + private void locationFirstRun() { + if (RecordLocationPreference.isSet(mPreferences)) { + return; + } + if (mActivity.isSecureCamera()) return; + // Check if the back camera exists + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId == -1) { + // If there is no back camera, do not show the prompt. + return; + } + + new AlertDialog.Builder(mActivity) + .setTitle(R.string.remember_location_title) + .setMessage(R.string.remember_location_prompt) + .setPositiveButton(R.string.remember_location_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + setLocationPreference(RecordLocationPreference.VALUE_ON); + } + }) + .setNegativeButton(R.string.remember_location_no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + dialog.cancel(); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + setLocationPreference(RecordLocationPreference.VALUE_OFF); + } + }) + .show(); + } + + private void setLocationPreference(String value) { + mPreferences.edit() + .putString(CameraSettings.KEY_RECORD_LOCATION, value) + .apply(); + // TODO: Fix this to use the actual onSharedPreferencesChanged listener + // instead of invoking manually + onSharedPreferenceChanged(); + } + + private void initializeRenderOverlay() { + if (mPieRenderer != null) { + mRenderOverlay.addRenderer(mPieRenderer); + mFocusManager.setFocusRenderer(mPieRenderer); + } + if (mZoomRenderer != null) { + mRenderOverlay.addRenderer(mZoomRenderer); + } + if (mGestures != null) { + mGestures.clearTouchReceivers(); + mGestures.setRenderOverlay(mRenderOverlay); + mGestures.addTouchReceiver(mMenu); + mGestures.addTouchReceiver(mBlocker); + + if (isImageCaptureIntent()) { + if (mReviewCancelButton != null) { + mGestures.addTouchReceiver((View) mReviewCancelButton); + } + if (mReviewDoneButton != null) { + mGestures.addTouchReceiver((View) mReviewDoneButton); + } + } + } + mRenderOverlay.requestLayout(); + } + + private void initializeAfterCameraOpen() { + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mPhotoControl = new PhotoController(mActivity, this, mPieRenderer); + mPhotoControl.setListener(this); + mPieRenderer.setPieListener(this); + } + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + } + if (mGestures == null) { + // this will handle gesture disambiguation and dispatching + mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer); + } + initializeRenderOverlay(); + initializePhotoControl(); + + // These depend on camera parameters. + setPreviewFrameLayoutAspectRatio(); + mFocusManager.setPreviewSize(mPreviewFrameLayout.getWidth(), + mPreviewFrameLayout.getHeight()); + loadCameraPreferences(); + initializeZoom(); + updateOnScreenIndicators(); + showTapToFocusToastIfNeeded(); + onFullScreenChanged(mActivity.isInCameraApp()); + } + + private void initializePhotoControl() { + loadCameraPreferences(); + if (mPhotoControl != null) { + mPhotoControl.initialize(mPreferenceGroup); + } + updateSceneModeUI(); + } + + + private void resetExposureCompensation() { + String value = mPreferences.getString(CameraSettings.KEY_EXPOSURE, + CameraSettings.EXPOSURE_DEFAULT_VALUE); + if (!CameraSettings.EXPOSURE_DEFAULT_VALUE.equals(value)) { + Editor editor = mPreferences.edit(); + editor.putString(CameraSettings.KEY_EXPOSURE, "0"); + editor.apply(); + } + } + + private void keepMediaProviderInstance() { + // We want to keep a reference to MediaProvider in camera's lifecycle. + // TODO: Utilize mMediaProviderClient instance to replace + // ContentResolver calls. + if (mMediaProviderClient == null) { + mMediaProviderClient = mContentResolver + .acquireContentProviderClient(MediaStore.AUTHORITY); + } + } + + // Snapshots can only be taken after this is called. It should be called + // once only. We could have done these things in onCreate() but we want to + // make preview screen appear as soon as possible. + private void initializeFirstTime() { + if (mFirstTimeInitialized) return; + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + keepMediaProviderInstance(); + + // Initialize shutter button. + mShutterButton = mActivity.getShutterButton(); + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(this); + mShutterButton.setVisibility(View.VISIBLE); + + mMediaSaver = new MediaSaver(mContentResolver); + mNamedImages = new NamedImages(); + + mFirstTimeInitialized = true; + addIdleHandler(); + + mActivity.updateStorageSpaceAndHint(); + } + + private void showTapToFocusToastIfNeeded() { + // Show the tap to focus toast if this is the first start. + if (mFocusAreaSupported && + mPreferences.getBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, true)) { + // Delay the toast for one second to wait for orientation. + mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_FOCUS_TOAST, 1000); + } + } + + private void addIdleHandler() { + MessageQueue queue = Looper.myQueue(); + queue.addIdleHandler(new MessageQueue.IdleHandler() { + @Override + public boolean queueIdle() { + Storage.ensureOSXCompatible(); + return false; + } + }); + } + + // If the activity is paused and resumed, this method will be called in + // onResume. + private void initializeSecondTime() { + + // Start location update if needed. + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + mMediaSaver = new MediaSaver(mContentResolver); + mNamedImages = new NamedImages(); + initializeZoom(); + keepMediaProviderInstance(); + hidePostCaptureAlert(); + + if (mPhotoControl != null) { + mPhotoControl.reloadPreferences(); + } + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return; + mZoomValue = index; + if (mParameters == null || mCameraDevice == null) return; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mCameraDevice.setParametersAsync(mParameters); + if (mZoomRenderer != null) { + Parameters p = mCameraDevice.getParameters(); + mZoomRenderer.setZoomValue(mZoomRatios.get(p.getZoom())); + } + } + + @Override + public void onZoomStart() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(true); + } + } + + @Override + public void onZoomEnd() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(false); + } + } + } + + private void initializeZoom() { + if ((mParameters == null) || !mParameters.isZoomSupported() + || (mZoomRenderer == null)) return; + mZoomMax = mParameters.getMaxZoom(); + mZoomRatios = mParameters.getZoomRatios(); + // Currently we use immediate zoom for fast zooming to get better UX and + // there is no plan to take advantage of the smooth zoom. + if (mZoomRenderer != null) { + mZoomRenderer.setZoomMax(mZoomMax); + mZoomRenderer.setZoom(mParameters.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(mParameters.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void startFaceDetection() { + if (!ApiHelper.HAS_FACE_DETECTION) return; + if (mFaceDetectionStarted) return; + if (mParameters.getMaxNumDetectedFaces() > 0) { + mFaceDetectionStarted = true; + mFaceView.clear(); + mFaceView.setVisibility(View.VISIBLE); + mFaceView.setDisplayOrientation(mDisplayOrientation); + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + mFaceView.setMirror(info.facing == CameraInfo.CAMERA_FACING_FRONT); + mFaceView.resume(); + mFocusManager.setFaceView(mFaceView); + mCameraDevice.setFaceDetectionListener(new FaceDetectionListener() { + @Override + public void onFaceDetection(Face[] faces, android.hardware.Camera camera) { + mFaceView.setFaces(faces); + } + }); + mCameraDevice.startFaceDetection(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void stopFaceDetection() { + if (!ApiHelper.HAS_FACE_DETECTION) return; + if (!mFaceDetectionStarted) return; + if (mParameters.getMaxNumDetectedFaces() > 0) { + mFaceDetectionStarted = false; + mCameraDevice.setFaceDetectionListener(null); + mCameraDevice.stopFaceDetection(); + if (mFaceView != null) mFaceView.clear(); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mCameraState == SWITCHING_CAMERA) return true; + if (mPopup != null) { + return mActivity.superDispatchTouchEvent(m); + } else if (mGestures != null && mRenderOverlay != null) { + return mGestures.dispatchTouch(m); + } + return false; + } + + private void initOnScreenIndicator() { + mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators); + mExposureIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_exposure_indicator); + mFlashIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_flash_indicator); + mSceneIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_scenemode_indicator); + mHdrIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_hdr_indicator); + } + + @Override + public void showGpsOnScreenIndicator(boolean hasSignal) { } + + @Override + public void hideGpsOnScreenIndicator() { } + + private void updateExposureOnScreenIndicator(int value) { + if (mExposureIndicator == null) { + return; + } + int id = 0; + float step = mParameters.getExposureCompensationStep(); + value = (int) Math.round(value * step); + switch(value) { + case -3: + id = R.drawable.ic_indicator_ev_n3; + break; + case -2: + id = R.drawable.ic_indicator_ev_n2; + break; + case -1: + id = R.drawable.ic_indicator_ev_n1; + break; + case 0: + id = R.drawable.ic_indicator_ev_0; + break; + case 1: + id = R.drawable.ic_indicator_ev_p1; + break; + case 2: + id = R.drawable.ic_indicator_ev_p2; + break; + case 3: + id = R.drawable.ic_indicator_ev_p3; + break; + } + mExposureIndicator.setImageResource(id); + + } + + private void updateFlashOnScreenIndicator(String value) { + if (mFlashIndicator == null) { + return; + } + if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } else { + if (Parameters.FLASH_MODE_AUTO.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto); + } else if (Parameters.FLASH_MODE_ON.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on); + } else { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } + } + } + + private void updateSceneOnScreenIndicator(String value) { + if (mSceneIndicator == null) { + return; + } + if ((value == null) || Parameters.SCENE_MODE_AUTO.equals(value) + || Parameters.SCENE_MODE_HDR.equals(value)) { + mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_off); + } else { + mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_on); + } + } + + private void updateHdrOnScreenIndicator(String value) { + if (mHdrIndicator == null) { + return; + } + if ((value != null) && Parameters.SCENE_MODE_HDR.equals(value)) { + mHdrIndicator.setImageResource(R.drawable.ic_indicator_hdr_on); + } else { + mHdrIndicator.setImageResource(R.drawable.ic_indicator_hdr_off); + } + } + + private void updateOnScreenIndicators() { + if (mParameters == null) return; + updateSceneOnScreenIndicator(mParameters.getSceneMode()); + updateExposureOnScreenIndicator(CameraSettings.readExposure(mPreferences)); + updateFlashOnScreenIndicator(mParameters.getFlashMode()); + updateHdrOnScreenIndicator(mParameters.getSceneMode()); + } + + private final class ShutterCallback + implements android.hardware.Camera.ShutterCallback { + @Override + public void onShutter() { + mShutterCallbackTime = System.currentTimeMillis(); + mShutterLag = mShutterCallbackTime - mCaptureStartTime; + Log.v(TAG, "mShutterLag = " + mShutterLag + "ms"); + } + } + + private final class PostViewPictureCallback implements PictureCallback { + @Override + public void onPictureTaken( + byte [] data, android.hardware.Camera camera) { + mPostViewPictureCallbackTime = System.currentTimeMillis(); + Log.v(TAG, "mShutterToPostViewCallbackTime = " + + (mPostViewPictureCallbackTime - mShutterCallbackTime) + + "ms"); + } + } + + private final class RawPictureCallback implements PictureCallback { + @Override + public void onPictureTaken( + byte [] rawData, android.hardware.Camera camera) { + mRawPictureCallbackTime = System.currentTimeMillis(); + Log.v(TAG, "mShutterToRawCallbackTime = " + + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms"); + } + } + + private final class JpegPictureCallback implements PictureCallback { + Location mLocation; + + public JpegPictureCallback(Location loc) { + mLocation = loc; + } + + @Override + public void onPictureTaken( + final byte [] jpegData, final android.hardware.Camera camera) { + if (mPaused) { + return; + } + if (mSceneMode == Util.SCENE_MODE_HDR) { + mActivity.showSwitcher(); + mActivity.setSwipingEnabled(true); + } + + mJpegPictureCallbackTime = System.currentTimeMillis(); + // If postview callback has arrived, the captured image is displayed + // in postview callback. If not, the captured image is displayed in + // raw picture callback. + if (mPostViewPictureCallbackTime != 0) { + mShutterToPictureDisplayedTime = + mPostViewPictureCallbackTime - mShutterCallbackTime; + mPictureDisplayedToJpegCallbackTime = + mJpegPictureCallbackTime - mPostViewPictureCallbackTime; + } else { + mShutterToPictureDisplayedTime = + mRawPictureCallbackTime - mShutterCallbackTime; + mPictureDisplayedToJpegCallbackTime = + mJpegPictureCallbackTime - mRawPictureCallbackTime; + } + Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = " + + mPictureDisplayedToJpegCallbackTime + "ms"); + + // Only animate when in full screen capture mode + // i.e. If monkey/a user swipes to the gallery during picture taking, + // don't show animation + if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent + && mActivity.mShowCameraAppView) { + // Finish capture animation + ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide(); + } + mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden. + if (!mIsImageCaptureIntent) { + if (ApiHelper.CAN_START_PREVIEW_IN_JPEG_CALLBACK) { + setupPreview(); + } else { + // Camera HAL of some devices have a bug. Starting preview + // immediately after taking a picture will fail. Wait some + // time before starting the preview. + mHandler.sendEmptyMessageDelayed(SETUP_PREVIEW, 300); + } + } + + if (!mIsImageCaptureIntent) { + // Calculate the width and the height of the jpeg. + Size s = mParameters.getPictureSize(); + int orientation = Exif.getOrientation(jpegData); + int width, height; + if ((mJpegRotation + orientation) % 180 == 0) { + width = s.width; + height = s.height; + } else { + width = s.height; + height = s.width; + } + String title = mNamedImages.getTitle(); + long date = mNamedImages.getDate(); + if (title == null) { + Log.e(TAG, "Unbalanced name/data pair"); + } else { + if (date == -1) date = mCaptureStartTime; + mMediaSaver.addImage(jpegData, title, date, mLocation, width, height, + orientation, mOnMediaSavedListener); + } + } else { + mJpegImageData = jpegData; + if (!mQuickCapture) { + showPostCaptureAlert(); + } else { + doAttach(); + } + } + + // Check this in advance of each shot so we don't add to shutter + // latency. It's true that someone else could write to the SD card in + // the mean time and fill it, but that could have happened between the + // shutter press and saving the JPEG too. + mActivity.updateStorageSpaceAndHint(); + + long now = System.currentTimeMillis(); + mJpegCallbackFinishTime = now - mJpegPictureCallbackTime; + Log.v(TAG, "mJpegCallbackFinishTime = " + + mJpegCallbackFinishTime + "ms"); + mJpegPictureCallbackTime = 0; + } + } + + private final class AutoFocusCallback + implements android.hardware.Camera.AutoFocusCallback { + @Override + public void onAutoFocus( + boolean focused, android.hardware.Camera camera) { + if (mPaused) return; + + mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime; + Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms"); + setCameraState(IDLE); + mFocusManager.onAutoFocus(focused, mShutterButton.isPressed()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private final class AutoFocusMoveCallback + implements android.hardware.Camera.AutoFocusMoveCallback { + @Override + public void onAutoFocusMoving( + boolean moving, android.hardware.Camera camera) { + mFocusManager.onAutoFocusMoving(moving); + } + } + + private static class NamedImages { + private ArrayList mQueue; + private boolean mStop; + private NamedEntity mNamedEntity; + + public NamedImages() { + mQueue = new ArrayList(); + } + + public void nameNewImage(ContentResolver resolver, long date) { + NamedEntity r = new NamedEntity(); + r.title = Util.createJpegName(date); + r.date = date; + mQueue.add(r); + } + + public String getTitle() { + if (mQueue.isEmpty()) { + mNamedEntity = null; + return null; + } + mNamedEntity = mQueue.get(0); + mQueue.remove(0); + + return mNamedEntity.title; + } + + // Must be called after getTitle(). + public long getDate() { + if (mNamedEntity == null) return -1; + return mNamedEntity.date; + } + + private static class NamedEntity { + String title; + long date; + } + } + + private void setCameraState(int state) { + mCameraState = state; + switch (state) { + case PREVIEW_STOPPED: + case SNAPSHOT_IN_PROGRESS: + case FOCUSING: + case SWITCHING_CAMERA: + if (mGestures != null) mGestures.setEnabled(false); + break; + case IDLE: + if (mGestures != null && mActivity.mShowCameraAppView) { + // Enable gestures only when the camera app view is visible + mGestures.setEnabled(true); + } + break; + } + } + + private void animateFlash() { + // Only animate when in full screen capture mode + // i.e. If monkey/a user swipes to the gallery during picture taking, + // don't show animation + if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent + && mActivity.mShowCameraAppView) { + // Start capture animation. + ((CameraScreenNail) mActivity.mCameraScreenNail).animateFlash(mDisplayRotation); + } + } + + @Override + public boolean capture() { + // If we are already in the middle of taking a snapshot or the image save request + // is full then ignore. + if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS + || mCameraState == SWITCHING_CAMERA || mMediaSaver.queueFull()) { + return false; + } + mCaptureStartTime = System.currentTimeMillis(); + mPostViewPictureCallbackTime = 0; + mJpegImageData = null; + + final boolean animateBefore = (mSceneMode == Util.SCENE_MODE_HDR); + + if (animateBefore) { + animateFlash(); + } + + // Set rotation and gps data. + mJpegRotation = Util.getJpegRotation(mCameraId, mOrientation); + mParameters.setRotation(mJpegRotation); + Location loc = mLocationManager.getCurrentLocation(); + Util.setGpsParameters(mParameters, loc); + mCameraDevice.setParameters(mParameters); + + mCameraDevice.takePicture2(mShutterCallback, mRawPictureCallback, + mPostViewPictureCallback, new JpegPictureCallback(loc), + mCameraState, mFocusManager.getFocusState()); + + if (!animateBefore) { + animateFlash(); + } + + mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime); + + mFaceDetectionStarted = false; + setCameraState(SNAPSHOT_IN_PROGRESS); + return true; + } + + @Override + public void setFocusParameters() { + setCameraParameters(UPDATE_PARAM_PREFERENCE); + } + + private int getPreferredCameraId(ComboPreferences preferences) { + int intentCameraId = Util.getCameraFacingIntentExtras(mActivity); + if (intentCameraId != -1) { + // Testing purpose. Launch a specific camera through the intent + // extras. + return intentCameraId; + } else { + return CameraSettings.readPreferredCameraId(preferences); + } + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenu != null) { + mMenu.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + @Override + public void onFullScreenChanged(boolean full) { + if (mFaceView != null) { + mFaceView.setBlockDraw(!full); + } + if (mPopup != null) { + dismissPopup(false, full); + } + if (mGestures != null) { + mGestures.setEnabled(full); + } + if (mRenderOverlay != null) { + // this can not happen in capture mode + mRenderOverlay.setVisibility(full ? View.VISIBLE : View.GONE); + } + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(!full); + } + setShowMenu(full); + if (mBlocker != null) { + mBlocker.setVisibility(full ? View.VISIBLE : View.GONE); + } + if (!full && mCountDownView != null) mCountDownView.cancelCountDown(); + if (ApiHelper.HAS_SURFACE_TEXTURE) { + if (mActivity.mCameraScreenNail != null) { + ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full); + } + return; + } + if (full) { + mPreviewSurfaceView.expand(); + } else { + mPreviewSurfaceView.shrink(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.v(TAG, "surfaceChanged:" + holder + " width=" + width + ". height=" + + height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.v(TAG, "surfaceCreated: " + holder); + mCameraSurfaceHolder = holder; + // Do not access the camera if camera start up thread is not finished. + if (mCameraDevice == null || mCameraStartUpThread != null) return; + + mCameraDevice.setPreviewDisplayAsync(holder); + // This happens when onConfigurationChanged arrives, surface has been + // destroyed, and there is no onFullScreenChanged. + if (mCameraState == PREVIEW_STOPPED) { + setupPreview(); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v(TAG, "surfaceDestroyed: " + holder); + mCameraSurfaceHolder = null; + stopPreview(); + } + + private void updateSceneModeUI() { + // If scene mode is set, we cannot set flash mode, white balance, and + // focus mode, instead, we read it from driver + if (!Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) { + overrideCameraSettings(mParameters.getFlashMode(), + mParameters.getWhiteBalance(), mParameters.getFocusMode()); + } else { + overrideCameraSettings(null, null, null); + } + } + + private void overrideCameraSettings(final String flashMode, + final String whiteBalance, final String focusMode) { + if (mPhotoControl != null) { +// mPieControl.enableFilter(true); + mPhotoControl.overrideSettings( + CameraSettings.KEY_FLASH_MODE, flashMode, + CameraSettings.KEY_WHITE_BALANCE, whiteBalance, + CameraSettings.KEY_FOCUS_MODE, focusMode); +// mPieControl.enableFilter(false); + } + } + + private void loadCameraPreferences() { + CameraSettings settings = new CameraSettings(mActivity, mInitialParams, + mCameraId, CameraHolder.instance().getCameraInfo()); + mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences); + } + + @Override + public boolean collapseCameraControls() { + // Remove all the popups/dialog boxes + boolean ret = false; + if (mPopup != null) { + dismissPopup(false); + ret = true; + } + return ret; + } + + public boolean removeTopLevelPopup() { + // Remove the top level popup or dialog box and return true if there's any + if (mPopup != null) { + dismissPopup(true); + return true; + } + return false; + } + + @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 == OrientationEventListener.ORIENTATION_UNKNOWN) return; + mOrientation = Util.roundOrientation(orientation, mOrientation); + + // Show the toast after getting the first orientation changed. + if (mHandler.hasMessages(SHOW_TAP_TO_FOCUS_TOAST)) { + mHandler.removeMessages(SHOW_TAP_TO_FOCUS_TOAST); + showTapToFocusToast(); + } + } + + @Override + public void onStop() { + if (mMediaProviderClient != null) { + mMediaProviderClient.release(); + mMediaProviderClient = null; + } + } + + // onClick handler for R.id.btn_done + @OnClickAttr + public void onReviewDoneClicked(View v) { + doAttach(); + } + + // onClick handler for R.id.btn_cancel + @OnClickAttr + public void onReviewCancelClicked(View v) { + doCancel(); + } + + // onClick handler for R.id.btn_retake + @OnClickAttr + public void onReviewRetakeClicked(View v) { + if (mPaused) + return; + + hidePostCaptureAlert(); + setupPreview(); + } + + private void doAttach() { + if (mPaused) { + return; + } + + byte[] data = mJpegImageData; + + if (mCropValue == null) { + // First handle the no crop case -- just return the value. If the + // caller specifies a "save uri" then write the data to its + // stream. Otherwise, pass back a scaled down version of the bitmap + // directly in the extras. + if (mSaveUri != null) { + OutputStream outputStream = null; + try { + outputStream = mContentResolver.openOutputStream(mSaveUri); + outputStream.write(data); + outputStream.close(); + + mActivity.setResultEx(Activity.RESULT_OK); + mActivity.finish(); + } catch (IOException ex) { + // ignore exception + } finally { + Util.closeSilently(outputStream); + } + } else { + int orientation = Exif.getOrientation(data); + Bitmap bitmap = Util.makeBitmap(data, 50 * 1024); + bitmap = Util.rotate(bitmap, orientation); + mActivity.setResultEx(Activity.RESULT_OK, + new Intent("inline-data").putExtra("data", bitmap)); + mActivity.finish(); + } + } else { + // Save the image to a temp file and invoke the cropper + Uri tempUri = null; + FileOutputStream tempStream = null; + try { + File path = mActivity.getFileStreamPath(sTempCropFilename); + path.delete(); + tempStream = mActivity.openFileOutput(sTempCropFilename, 0); + tempStream.write(data); + tempStream.close(); + tempUri = Uri.fromFile(path); + } catch (FileNotFoundException ex) { + mActivity.setResultEx(Activity.RESULT_CANCELED); + mActivity.finish(); + return; + } catch (IOException ex) { + mActivity.setResultEx(Activity.RESULT_CANCELED); + mActivity.finish(); + return; + } finally { + Util.closeSilently(tempStream); + } + + Bundle newExtras = new Bundle(); + if (mCropValue.equals("circle")) { + newExtras.putString("circleCrop", "true"); + } + if (mSaveUri != null) { + newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, mSaveUri); + } else { + newExtras.putBoolean(CropExtras.KEY_RETURN_DATA, true); + } + if (mActivity.isSecureCamera()) { + newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true); + } + + Intent cropIntent = new Intent(FilterShowActivity.CROP_ACTION); + + cropIntent.setData(tempUri); + cropIntent.putExtras(newExtras); + + mActivity.startActivityForResult(cropIntent, REQUEST_CROP); + } + } + + private void doCancel() { + mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent()); + mActivity.finish(); + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + if (mPaused || collapseCameraControls() + || (mCameraState == SNAPSHOT_IN_PROGRESS) + || (mCameraState == PREVIEW_STOPPED)) return; + + // Do not do focus if there is not enough storage. + if (pressed && !canTakePicture()) return; + + if (pressed) { + if (mSceneMode == Util.SCENE_MODE_HDR) { + mActivity.hideSwitcher(); + mActivity.setSwipingEnabled(false); + } + mFocusManager.onShutterDown(); + } else { + mFocusManager.onShutterUp(); + } + } + + @Override + public void onShutterButtonClick() { + if (mPaused || collapseCameraControls() + || (mCameraState == SWITCHING_CAMERA) + || (mCameraState == PREVIEW_STOPPED)) return; + + // Do not take the picture if there is not enough storage. + if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) { + Log.i(TAG, "Not enough space or storage not ready. remaining=" + + mActivity.getStorageSpace()); + return; + } + Log.v(TAG, "onShutterButtonClick: mCameraState=" + mCameraState); + + // If the user wants to do a snapshot while the previous one is still + // in progress, remember the fact and do it after we finish the previous + // one and re-start the preview. Snapshot in progress also includes the + // state that autofocus is focusing and a picture will be taken when + // focus callback arrives. + if ((mFocusManager.isFocusingSnapOnFinish() || mCameraState == SNAPSHOT_IN_PROGRESS) + && !mIsImageCaptureIntent) { + mSnapshotOnIdle = true; + return; + } + + String timer = mPreferences.getString( + CameraSettings.KEY_TIMER, + mActivity.getString(R.string.pref_camera_timer_default)); + boolean playSound = mPreferences.getString(CameraSettings.KEY_TIMER_SOUND_EFFECTS, + mActivity.getString(R.string.pref_camera_timer_sound_default)) + .equals(mActivity.getString(R.string.setting_on_value)); + + int seconds = Integer.parseInt(timer); + // When shutter button is pressed, check whether the previous countdown is + // finished. If not, cancel the previous countdown and start a new one. + if (mCountDownView.isCountingDown()) { + mCountDownView.cancelCountDown(); + mCountDownView.startCountDown(seconds, playSound); + } else if (seconds > 0) { + mCountDownView.startCountDown(seconds, playSound); + } else { + mSnapshotOnIdle = false; + mFocusManager.doSnap(); + } + } + + @Override + public void installIntentFilter() { + } + + @Override + public boolean updateStorageHintOnResume() { + return mFirstTimeInitialized; + } + + @Override + public void updateCameraAppView() { + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + if (mOpenCameraFail || mCameraDisabled) return; + + mJpegPictureCallbackTime = 0; + mZoomValue = 0; + + // Start the preview if it is not started. + if (mCameraState == PREVIEW_STOPPED && mCameraStartUpThread == null) { + resetExposureCompensation(); + mCameraStartUpThread = new CameraStartUpThread(); + mCameraStartUpThread.start(); + } + + // If first time initialization is not finished, put it in the + // message queue. + if (!mFirstTimeInitialized) { + mHandler.sendEmptyMessage(FIRST_TIME_INIT); + } else { + initializeSecondTime(); + } + keepScreenOnAwhile(); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + } + + void waitCameraStartUpThread() { + try { + if (mCameraStartUpThread != null) { + mCameraStartUpThread.cancel(); + mCameraStartUpThread.join(); + mCameraStartUpThread = null; + setCameraState(IDLE); + } + } catch (InterruptedException e) { + // ignore + } + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + } + + @Override + public void onPauseAfterSuper() { + // Wait the camera start up thread to finish. + waitCameraStartUpThread(); + + // When camera is started from secure lock screen for the first time + // after screen on, the activity gets onCreate->onResume->onPause->onResume. + // To reduce the latency, keep the camera for a short time so it does + // not need to be opened again. + if (mCameraDevice != null && mActivity.isSecureCamera() + && ActivityBase.isFirstStartAfterScreenOn()) { + ActivityBase.resetFirstStartAfterScreenOn(); + CameraHolder.instance().keep(KEEP_CAMERA_TIMEOUT); + } + // Reset the focus first. Camera CTS does not guarantee that + // cancelAutoFocus is allowed after preview stops. + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + mCameraDevice.cancelAutoFocus(); + } + stopPreview(); + mCountDownView.cancelCountDown(); + // Close the camera now because other activities may need to use it. + closeCamera(); + if (mSurfaceTexture != null) { + ((CameraScreenNail) mActivity.mCameraScreenNail).releaseSurfaceTexture(); + mSurfaceTexture = null; + } + resetScreenOn(); + + // Clear UI. + collapseCameraControls(); + if (mFaceView != null) mFaceView.clear(); + + if (mFirstTimeInitialized) { + if (mMediaSaver != null) { + mMediaSaver.finish(); + mMediaSaver = null; + mNamedImages = null; + } + } + + if (mLocationManager != null) mLocationManager.recordLocation(false); + + // If we are in an image capture intent and has taken + // a picture, we just clear it in onPause. + mJpegImageData = null; + + // Remove the messages in the event queue. + mHandler.removeMessages(SETUP_PREVIEW); + mHandler.removeMessages(FIRST_TIME_INIT); + mHandler.removeMessages(CHECK_DISPLAY_ROTATION); + mHandler.removeMessages(SWITCH_CAMERA); + mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION); + mHandler.removeMessages(CAMERA_OPEN_DONE); + mHandler.removeMessages(START_PREVIEW_DONE); + mHandler.removeMessages(OPEN_CAMERA_FAIL); + mHandler.removeMessages(CAMERA_DISABLED); + + mPendingSwitchCameraId = -1; + if (mFocusManager != null) mFocusManager.removeMessages(); + } + + private void initializeControlByIntent() { + mBlocker = mRootView.findViewById(R.id.blocker); + mMenu = mRootView.findViewById(R.id.menu); + mMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mPieRenderer != null) { + // If autofocus is not finished, cancel autofocus so that the + // subsequent touch can be handled by PreviewGestures + if (mCameraState == FOCUSING) cancelAutoFocus(); + mPieRenderer.showInCenter(); + } + } + }); + if (mIsImageCaptureIntent) { + + mActivity.hideSwitcher(); + // Cannot use RotateImageView for "done" and "cancel" button because + // the tablet layout uses RotateLayout, which cannot be cast to + // RotateImageView. + mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done); + mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel); + mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake); + ((View) mReviewCancelButton).setVisibility(View.VISIBLE); + + ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewDoneClicked(v); + } + }); + ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewCancelClicked(v); + } + }); + + mReviewRetakeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewRetakeClicked(v); + } + }); + + // Not grayed out upon disabled, to make the follow-up fade-out + // effect look smooth. Note that the review done button in tablet + // layout is not a TwoStateImageView. + if (mReviewDoneButton instanceof TwoStateImageView) { + ((TwoStateImageView) mReviewDoneButton).enableFilter(false); + } + + setupCaptureParams(); + } + } + + /** + * The focus manager is the first UI related element to get initialized, + * and it requires the RenderOverlay, so initialize it here + */ + private void initializeFocusManager() { + // Create FocusManager object. startPreview needs it. + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + // if mFocusManager not null, reuse it + // otherwise create a new instance + if (mFocusManager != null) { + mFocusManager.removeMessages(); + } else { + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT); + String[] defaultFocusModes = mActivity.getResources().getStringArray( + R.array.pref_camera_focusmode_default_array); + mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes, + mInitialParams, this, mirror, + mActivity.getMainLooper()); + } + } + + private void initializeMiscControls() { + // startPreview needs this. + mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame); + // Set touch focus listener. + mActivity.setSingleTapUpListener(mPreviewFrameLayout); + + mFaceView = (FaceView) mRootView.findViewById(R.id.face_view); + mPreviewFrameLayout.setOnSizeChangedListener(this); + mPreviewFrameLayout.setOnLayoutChangeListener(mActivity); + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + mPreviewSurfaceView = + (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view); + mPreviewSurfaceView.setVisibility(View.VISIBLE); + mPreviewSurfaceView.getHolder().addCallback(this); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged"); + setDisplayOrientation(); + + // Only the views in photo_module_content need to be removed and recreated + // i.e. CountDownView won't be recreated + ViewGroup viewGroup = (ViewGroup) mRootView.findViewById(R.id.camera_app); + viewGroup.removeAllViews(); + LayoutInflater inflater = mActivity.getLayoutInflater(); + inflater.inflate(R.layout.photo_module_content, (ViewGroup) viewGroup); + + // from onCreate() + initializeControlByIntent(); + + initializeFocusManager(); + initializeMiscControls(); + loadCameraPreferences(); + + // from initializeFirstTime() + mShutterButton = mActivity.getShutterButton(); + mShutterButton.setOnShutterButtonListener(this); + initializeZoom(); + initOnScreenIndicator(); + updateOnScreenIndicators(); + if (mFaceView != null) { + mFaceView.clear(); + mFaceView.setVisibility(View.VISIBLE); + mFaceView.setDisplayOrientation(mDisplayOrientation); + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + mFaceView.setMirror(info.facing == CameraInfo.CAMERA_FACING_FRONT); + mFaceView.resume(); + mFocusManager.setFaceView(mFaceView); + } + initializeRenderOverlay(); + onFullScreenChanged(mActivity.isInCameraApp()); + if (mJpegImageData != null) { // Jpeg data found, picture has been taken. + showPostCaptureAlert(); + } + } + + @Override + public void onActivityResult( + int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CROP: { + Intent intent = new Intent(); + if (data != null) { + Bundle extras = data.getExtras(); + if (extras != null) { + intent.putExtras(extras); + } + } + mActivity.setResultEx(resultCode, intent); + mActivity.finish(); + + File path = mActivity.getFileStreamPath(sTempCropFilename); + path.delete(); + + break; + } + } + } + + private boolean canTakePicture() { + return isCameraIdle() && (mActivity.getStorageSpace() > Storage.LOW_STORAGE_THRESHOLD); + } + + @Override + public void autoFocus() { + mFocusStartTime = System.currentTimeMillis(); + mCameraDevice.autoFocus(mAutoFocusCallback); + setCameraState(FOCUSING); + } + + @Override + public void cancelAutoFocus() { + mCameraDevice.cancelAutoFocus(); + setCameraState(IDLE); + setCameraParameters(UPDATE_PARAM_PREFERENCE); + } + + // Preview area is touched. Handle touch focus. + @Override + public void onSingleTapUp(View view, int x, int y) { + if (mPaused || mCameraDevice == null || !mFirstTimeInitialized + || mCameraState == SNAPSHOT_IN_PROGRESS + || mCameraState == SWITCHING_CAMERA + || mCameraState == PREVIEW_STOPPED) { + return; + } + + // Do not trigger touch focus if popup window is opened. + if (removeTopLevelPopup()) return; + + // Check if metering area or focus area is supported. + if (!mFocusAreaSupported && !mMeteringAreaSupported) return; + mFocusManager.onSingleTapUp(x, y); + } + + @Override + public boolean onBackPressed() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + return true; + } + // In image capture mode, back button should: + // 1) if there is any popup, dismiss them, 2) otherwise, get out of image capture + if (mIsImageCaptureIntent) { + if (!removeTopLevelPopup()) { + // no popup to dismiss, cancel image capture + doCancel(); + } + return true; + } else if (!isCameraIdle()) { + // ignore backs while we're taking a picture + return true; + } else { + return removeTopLevelPopup(); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_FOCUS: + if (mActivity.isInCameraApp() && mFirstTimeInitialized) { + if (event.getRepeatCount() == 0) { + onShutterButtonFocus(true); + } + return true; + } + return false; + case KeyEvent.KEYCODE_CAMERA: + if (mFirstTimeInitialized && event.getRepeatCount() == 0) { + onShutterButtonClick(); + } + return true; + case KeyEvent.KEYCODE_DPAD_CENTER: + // If we get a dpad center event without any focused view, move + // the focus to the shutter button and press it. + if (mFirstTimeInitialized && event.getRepeatCount() == 0) { + // Start auto-focus immediately to reduce shutter lag. After + // the shutter button gets the focus, onShutterButtonFocus() + // will be called again but it is fine. + if (removeTopLevelPopup()) return true; + onShutterButtonFocus(true); + if (mShutterButton.isInTouchMode()) { + mShutterButton.requestFocusFromTouch(); + } else { + mShutterButton.requestFocus(); + } + mShutterButton.setPressed(true); + } + return true; + } + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (mActivity.isInCameraApp() && mFirstTimeInitialized) { + onShutterButtonClick(); + return true; + } + return false; + case KeyEvent.KEYCODE_FOCUS: + if (mFirstTimeInitialized) { + onShutterButtonFocus(false); + } + return true; + } + return false; + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void closeCamera() { + if (mCameraDevice != null) { + mCameraDevice.setZoomChangeListener(null); + if(ApiHelper.HAS_FACE_DETECTION) { + mCameraDevice.setFaceDetectionListener(null); + } + mCameraDevice.setErrorCallback(null); + CameraHolder.instance().release(); + mFaceDetectionStarted = false; + mCameraDevice = null; + setCameraState(PREVIEW_STOPPED); + mFocusManager.onCameraReleased(); + } + } + + private void setDisplayOrientation() { + mDisplayRotation = Util.getDisplayRotation(mActivity); + mDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId); + mCameraDisplayOrientation = Util.getDisplayOrientation(0, mCameraId); + if (mFaceView != null) { + mFaceView.setDisplayOrientation(mDisplayOrientation); + } + if (mFocusManager != null) { + mFocusManager.setDisplayOrientation(mDisplayOrientation); + } + // GLRoot also uses the DisplayRotation, and needs to be told to layout to update + mActivity.getGLRoot().requestLayoutContentPane(); + } + + // Only called by UI thread. + private void setupPreview() { + mFocusManager.resetTouchFocus(); + startPreview(); + setCameraState(IDLE); + startFaceDetection(); + } + + // This can be called by UI Thread or CameraStartUpThread. So this should + // not modify the views. + private void startPreview() { + mCameraDevice.setErrorCallback(mErrorCallback); + + // ICS camera frameworks has a bug. Face detection state is not cleared + // after taking a picture. Stop the preview to work around it. The bug + // was fixed in JB. + if (mCameraState != PREVIEW_STOPPED) stopPreview(); + + setDisplayOrientation(); + + if (!mSnapshotOnIdle) { + // If the focus mode is continuous autofocus, call cancelAutoFocus to + // resume it because it may have been paused by autoFocus call. + if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) { + mCameraDevice.cancelAutoFocus(); + } + mFocusManager.setAeAwbLock(false); // Unlock AE and AWB. + } + setCameraParameters(UPDATE_PARAM_ALL); + + if (ApiHelper.HAS_SURFACE_TEXTURE) { + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + if (mSurfaceTexture == null) { + Size size = mParameters.getPreviewSize(); + if (mCameraDisplayOrientation % 180 == 0) { + screenNail.setSize(size.width, size.height); + } else { + screenNail.setSize(size.height, size.width); + } + screenNail.enableAspectRatioClamping(); + mActivity.notifyScreenNailChanged(); + screenNail.acquireSurfaceTexture(); + CameraStartUpThread t = mCameraStartUpThread; + if (t != null && t.isCanceled()) { + return; // Exiting, so no need to get the surface texture. + } + mSurfaceTexture = screenNail.getSurfaceTexture(); + } + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + if (mSurfaceTexture != null) { + mCameraDevice.setPreviewTextureAsync((SurfaceTexture) mSurfaceTexture); + } + } else { + mCameraDevice.setDisplayOrientation(mDisplayOrientation); + mCameraDevice.setPreviewDisplayAsync(mCameraSurfaceHolder); + } + + Log.v(TAG, "startPreview"); + mCameraDevice.startPreviewAsync(); + + mFocusManager.onPreviewStarted(); + + if (mSnapshotOnIdle) { + mHandler.post(mDoSnapRunnable); + } + } + + private void stopPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + Log.v(TAG, "stopPreview"); + mCameraDevice.stopPreview(); + mFaceDetectionStarted = false; + } + setCameraState(PREVIEW_STOPPED); + if (mFocusManager != null) mFocusManager.onPreviewStopped(); + } + + @SuppressWarnings("deprecation") + private void updateCameraParametersInitialize() { + // Reset preview frame rate to the maximum because it may be lowered by + // video camera application. + List frameRates = mParameters.getSupportedPreviewFrameRates(); + if (frameRates != null) { + Integer max = Collections.max(frameRates); + mParameters.setPreviewFrameRate(max); + } + + mParameters.set(Util.RECORDING_HINT, Util.FALSE); + + // Disable video stabilization. Convenience methods not available in API + // level <= 14 + String vstabSupported = mParameters.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + mParameters.set("video-stabilization", "false"); + } + } + + private void updateCameraParametersZoom() { + // Set zoom. + if (mParameters.isZoomSupported()) { + mParameters.setZoom(mZoomValue); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoExposureLockIfSupported() { + if (mAeLockSupported) { + mParameters.setAutoExposureLock(mFocusManager.getAeAwbLock()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoWhiteBalanceLockIfSupported() { + if (mAwbLockSupported) { + mParameters.setAutoWhiteBalanceLock(mFocusManager.getAeAwbLock()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setFocusAreasIfSupported() { + if (mFocusAreaSupported) { + mParameters.setFocusAreas(mFocusManager.getFocusAreas()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setMeteringAreasIfSupported() { + if (mMeteringAreaSupported) { + // Use the same area for focus and metering. + mParameters.setMeteringAreas(mFocusManager.getMeteringAreas()); + } + } + + private void updateCameraParametersPreference() { + setAutoExposureLockIfSupported(); + setAutoWhiteBalanceLockIfSupported(); + setFocusAreasIfSupported(); + setMeteringAreasIfSupported(); + + // Set picture size. + String pictureSize = mPreferences.getString( + CameraSettings.KEY_PICTURE_SIZE, null); + if (pictureSize == null) { + CameraSettings.initialCameraPictureSize(mActivity, mParameters); + } else { + List supported = mParameters.getSupportedPictureSizes(); + CameraSettings.setCameraPictureSize( + pictureSize, supported, mParameters); + } + Size size = mParameters.getPictureSize(); + + // Set a preview size that is closest to the viewfinder height and has + // the right aspect ratio. + List sizes = mParameters.getSupportedPreviewSizes(); + Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes, + (double) size.width / size.height); + Size original = mParameters.getPreviewSize(); + if (!original.equals(optimalSize)) { + mParameters.setPreviewSize(optimalSize.width, optimalSize.height); + + // Zoom related settings will be changed for different preview + // sizes, so set and read the parameters to get latest values + mCameraDevice.setParameters(mParameters); + mParameters = mCameraDevice.getParameters(); + } + Log.v(TAG, "Preview size is " + optimalSize.width + "x" + optimalSize.height); + + // Since changing scene mode may change supported values, set scene mode + // first. HDR is a scene mode. To promote it in UI, it is stored in a + // separate preference. + String hdr = mPreferences.getString(CameraSettings.KEY_CAMERA_HDR, + mActivity.getString(R.string.pref_camera_hdr_default)); + if (mActivity.getString(R.string.setting_on_value).equals(hdr)) { + mSceneMode = Util.SCENE_MODE_HDR; + } else { + mSceneMode = mPreferences.getString( + CameraSettings.KEY_SCENE_MODE, + mActivity.getString(R.string.pref_camera_scenemode_default)); + } + if (Util.isSupported(mSceneMode, mParameters.getSupportedSceneModes())) { + if (!mParameters.getSceneMode().equals(mSceneMode)) { + mParameters.setSceneMode(mSceneMode); + + // Setting scene mode will change the settings of flash mode, + // white balance, and focus mode. Here we read back the + // parameters, so we can know those settings. + mCameraDevice.setParameters(mParameters); + mParameters = mCameraDevice.getParameters(); + } + } else { + mSceneMode = mParameters.getSceneMode(); + if (mSceneMode == null) { + mSceneMode = Parameters.SCENE_MODE_AUTO; + } + } + + // Set JPEG quality. + int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, + CameraProfile.QUALITY_HIGH); + mParameters.setJpegQuality(jpegQuality); + + // For the following settings, we need to check if the settings are + // still supported by latest driver, if not, ignore the settings. + + // Set exposure compensation + int value = CameraSettings.readExposure(mPreferences); + int max = mParameters.getMaxExposureCompensation(); + int min = mParameters.getMinExposureCompensation(); + if (value >= min && value <= max) { + mParameters.setExposureCompensation(value); + } else { + Log.w(TAG, "invalid exposure range: " + value); + } + + if (Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) { + // Set flash mode. + String flashMode = mPreferences.getString( + CameraSettings.KEY_FLASH_MODE, + mActivity.getString(R.string.pref_camera_flashmode_default)); + List supportedFlash = mParameters.getSupportedFlashModes(); + if (Util.isSupported(flashMode, supportedFlash)) { + mParameters.setFlashMode(flashMode); + } else { + flashMode = mParameters.getFlashMode(); + if (flashMode == null) { + flashMode = mActivity.getString( + R.string.pref_camera_flashmode_no_flash); + } + } + + // Set white balance parameter. + String whiteBalance = mPreferences.getString( + CameraSettings.KEY_WHITE_BALANCE, + mActivity.getString(R.string.pref_camera_whitebalance_default)); + if (Util.isSupported(whiteBalance, + mParameters.getSupportedWhiteBalance())) { + mParameters.setWhiteBalance(whiteBalance); + } else { + whiteBalance = mParameters.getWhiteBalance(); + if (whiteBalance == null) { + whiteBalance = Parameters.WHITE_BALANCE_AUTO; + } + } + + // Set focus mode. + mFocusManager.overrideFocusMode(null); + mParameters.setFocusMode(mFocusManager.getFocusMode()); + } else { + mFocusManager.overrideFocusMode(mParameters.getFocusMode()); + } + + if (mContinousFocusSupported && ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK) { + updateAutoFocusMoveCallback(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void updateAutoFocusMoveCallback() { + if (mParameters.getFocusMode().equals(Util.FOCUS_MODE_CONTINUOUS_PICTURE)) { + mCameraDevice.setAutoFocusMoveCallback( + (AutoFocusMoveCallback) mAutoFocusMoveCallback); + } else { + mCameraDevice.setAutoFocusMoveCallback(null); + } + } + + // We separate the parameters into several subsets, so we can update only + // the subsets actually need updating. The PREFERENCE set needs extra + // locking because the preference can be changed from GLThread as well. + private void setCameraParameters(int updateSet) { + if ((updateSet & UPDATE_PARAM_INITIALIZE) != 0) { + updateCameraParametersInitialize(); + } + + if ((updateSet & UPDATE_PARAM_ZOOM) != 0) { + updateCameraParametersZoom(); + } + + if ((updateSet & UPDATE_PARAM_PREFERENCE) != 0) { + updateCameraParametersPreference(); + } + + mCameraDevice.setParameters(mParameters); + } + + // If the Camera is idle, update the parameters immediately, otherwise + // accumulate them in mUpdateSet and update later. + private void setCameraParametersWhenIdle(int additionalUpdateSet) { + mUpdateSet |= additionalUpdateSet; + if (mCameraDevice == null) { + // We will update all the parameters when we open the device, so + // we don't need to do anything now. + mUpdateSet = 0; + return; + } else if (isCameraIdle()) { + setCameraParameters(mUpdateSet); + updateSceneModeUI(); + mUpdateSet = 0; + } else { + if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) { + mHandler.sendEmptyMessageDelayed( + SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000); + } + } + } + + private boolean isCameraIdle() { + return (mCameraState == IDLE) || + (mCameraState == PREVIEW_STOPPED) || + ((mFocusManager != null) && mFocusManager.isFocusCompleted() + && (mCameraState != SWITCHING_CAMERA)); + } + + private boolean isImageCaptureIntent() { + String action = mActivity.getIntent().getAction(); + return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action) + || ActivityBase.ACTION_IMAGE_CAPTURE_SECURE.equals(action)); + } + + private void setupCaptureParams() { + Bundle myExtras = mActivity.getIntent().getExtras(); + if (myExtras != null) { + mSaveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + mCropValue = myExtras.getString("crop"); + } + } + + private void showPostCaptureAlert() { + if (mIsImageCaptureIntent) { + mOnScreenIndicators.setVisibility(View.GONE); + mMenu.setVisibility(View.GONE); + Util.fadeIn((View) mReviewDoneButton); + mShutterButton.setVisibility(View.INVISIBLE); + Util.fadeIn(mReviewRetakeButton); + } + } + + private void hidePostCaptureAlert() { + if (mIsImageCaptureIntent) { + mOnScreenIndicators.setVisibility(View.VISIBLE); + mMenu.setVisibility(View.VISIBLE); + Util.fadeOut((View) mReviewDoneButton); + mShutterButton.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewRetakeButton); + } + } + + @Override + public void onSharedPreferenceChanged() { + // ignore the events after "onPause()" + if (mPaused) return; + + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE); + setPreviewFrameLayoutAspectRatio(); + updateOnScreenIndicators(); + } + + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + if (ApiHelper.HAS_SURFACE_TEXTURE) { + Log.v(TAG, "Start to copy texture. cameraId=" + cameraId); + // We need to keep a preview frame for the animation before + // releasing the camera. This will trigger onPreviewTextureCopied. + ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); + // Disable all camera controls. + setCameraState(SWITCHING_CAMERA); + } else { + switchCamera(); + } + } + + private void switchCamera() { + if (mPaused) return; + + Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + mPhotoControl.setCameraId(mCameraId); + + // from onPause + closeCamera(); + collapseCameraControls(); + if (mFaceView != null) mFaceView.clear(); + if (mFocusManager != null) mFocusManager.removeMessages(); + + // Restart the camera and initialize the UI. From onCreate. + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + try { + mCameraDevice = Util.openCamera(mActivity, mCameraId); + mParameters = mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + initializeCapabilities(); + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT); + mFocusManager.setMirror(mirror); + mFocusManager.setParameters(mInitialParams); + setupPreview(); + loadCameraPreferences(); + initializePhotoControl(); + + // from initializeFirstTime + initializeZoom(); + updateOnScreenIndicators(); + showTapToFocusToastIfNeeded(); + + if (ApiHelper.HAS_SURFACE_TEXTURE) { + // Start switch camera animation. Post a message because + // onFrameAvailable from the old camera may already exist. + mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); + } + } + + @Override + public void onPieOpened(int centerX, int centerY) { + mActivity.cancelActivityTouchHandling(); + mActivity.setSwipingEnabled(false); + if (mFaceView != null) { + mFaceView.setBlockDraw(true); + } + } + + @Override + public void onPieClosed() { + mActivity.setSwipingEnabled(true); + if (mFaceView != null) { + mFaceView.setBlockDraw(false); + } + } + + // Preview texture has been copied. Now camera can be released and the + // animation can be started. + @Override + public void onPreviewTextureCopied() { + mHandler.sendEmptyMessage(SWITCH_CAMERA); + } + + @Override + public void onCaptureTextureCopied() { + } + + @Override + public void onUserInteraction() { + if (!mActivity.isFinishing()) keepScreenOnAwhile(); + } + + private void resetScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + // TODO: Delete this function after old camera code is removed + @Override + public void onRestorePreferencesClicked() { + } + + @Override + public void onOverriddenPreferencesClicked() { + if (mPaused) return; + if (mNotSelectableToast == null) { + String str = mActivity.getResources().getString(R.string.not_selectable_in_scene_mode); + mNotSelectableToast = Toast.makeText(mActivity, str, Toast.LENGTH_SHORT); + } + mNotSelectableToast.show(); + } + + private void showTapToFocusToast() { + // TODO: Use a toast? + new RotateTextToast(mActivity, R.string.tap_to_focus, 0).show(); + // Clear the preference. + Editor editor = mPreferences.edit(); + editor.putBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, false); + editor.apply(); + } + + private void initializeCapabilities() { + mInitialParams = mCameraDevice.getParameters(); + mFocusAreaSupported = Util.isFocusAreaSupported(mInitialParams); + mMeteringAreaSupported = Util.isMeteringAreaSupported(mInitialParams); + mAeLockSupported = Util.isAutoExposureLockSupported(mInitialParams); + mAwbLockSupported = Util.isAutoWhiteBalanceLockSupported(mInitialParams); + mContinousFocusSupported = mInitialParams.getSupportedFocusModes().contains( + Util.FOCUS_MODE_CONTINUOUS_PICTURE); + } + + // PreviewFrameLayout size has changed. + @Override + public void onSizeChanged(int width, int height) { + if (mFocusManager != null) mFocusManager.setPreviewSize(width, height); + } + + @Override + public void onCountDownFinished() { + mSnapshotOnIdle = false; + mFocusManager.doSnap(); + } + + void setPreviewFrameLayoutAspectRatio() { + // Set the preview frame aspect ratio according to the picture size. + Size size = mParameters.getPictureSize(); + mPreviewFrameLayout.setAspectRatio((double) size.width / size.height); + } + + @Override + public boolean needsSwitcher() { + return !mIsImageCaptureIntent; + } + + public void showPopup(AbstractSettingPopup popup) { + mActivity.hideUI(); + mBlocker.setVisibility(View.INVISIBLE); + setShowMenu(false); + mPopup = popup; + mPopup.setVisibility(View.VISIBLE); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER; + ((FrameLayout) mRootView).addView(mPopup, lp); + } + + public void dismissPopup(boolean topPopupOnly) { + dismissPopup(topPopupOnly, true); + } + + private void dismissPopup(boolean topOnly, boolean fullScreen) { + if (fullScreen) { + mActivity.showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mPhotoControl.popupDismissed(topOnly); + } + + @Override + public void onShowSwitcherPopup() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + +} diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java new file mode 100644 index 000000000..8202fca21 --- /dev/null +++ b/src/com/android/camera/PieController.java @@ -0,0 +1,191 @@ +/* + * 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; + +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.drawable.TextDrawable; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PieController { + + private static String TAG = "CAM_piecontrol"; + + protected static final int MODE_PHOTO = 0; + protected static final int MODE_VIDEO = 1; + + protected CameraActivity mActivity; + protected PreferenceGroup mPreferenceGroup; + protected OnPreferenceChangedListener mListener; + protected PieRenderer mRenderer; + private List mPreferences; + private Map mPreferenceMap; + private Map mOverrides; + + public void setListener(OnPreferenceChangedListener listener) { + mListener = listener; + } + + public PieController(CameraActivity activity, PieRenderer pie) { + mActivity = activity; + mRenderer = pie; + mPreferences = new ArrayList(); + mPreferenceMap = new HashMap(); + mOverrides = new HashMap(); + } + + public void initialize(PreferenceGroup group) { + mRenderer.clearItems(); + setPreferenceGroup(group); + } + + public void onSettingChanged(ListPreference pref) { + if (mListener != null) { + mListener.onSharedPreferenceChanged(); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + protected PieItem makeItem(int resId) { + // We need a mutable version as we change the alpha + Drawable d = mActivity.getResources().getDrawable(resId).mutate(); + return new PieItem(d, 0); + } + + protected PieItem makeItem(CharSequence value) { + TextDrawable drawable = new TextDrawable(mActivity.getResources(), value); + return new PieItem(drawable, 0); + } + + public void addItem(String prefKey, float center, float sweep) { + final IconListPreference pref = + (IconListPreference) mPreferenceGroup.findPreference(prefKey); + if (pref == null) return; + int[] iconIds = pref.getLargeIconIds(); + int resid = -1; + if (!pref.getUseSingleIcon() && iconIds != null) { + // Each entry has a corresponding icon. + int index = pref.findIndexOfValue(pref.getValue()); + resid = iconIds[index]; + } else { + // The preference only has a single icon to represent it. + resid = pref.getSingleIcon(); + } + PieItem item = makeItem(resid); + // use center and sweep to determine layout + item.setFixedSlice(center, sweep); + mRenderer.addItem(item); + mPreferences.add(pref); + mPreferenceMap.put(pref, item); + int nOfEntries = pref.getEntries().length; + if (nOfEntries > 1) { + for (int i = 0; i < nOfEntries; i++) { + PieItem inner = null; + if (iconIds != null) { + inner = makeItem(iconIds[i]); + } else { + inner = makeItem(pref.getEntries()[i]); + } + item.addItem(inner); + final int index = i; + inner.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + pref.setValueIndex(index); + reloadPreference(pref); + onSettingChanged(pref); + } + }); + } + } + } + + public void setPreferenceGroup(PreferenceGroup group) { + mPreferenceGroup = group; + } + + public void reloadPreferences() { + mPreferenceGroup.reloadValue(); + for (IconListPreference pref : mPreferenceMap.keySet()) { + reloadPreference(pref); + } + } + + private void reloadPreference(IconListPreference pref) { + if (pref.getUseSingleIcon()) return; + PieItem item = mPreferenceMap.get(pref); + String overrideValue = mOverrides.get(pref); + int[] iconIds = pref.getLargeIconIds(); + if (iconIds != null) { + // Each entry has a corresponding icon. + int index; + if (overrideValue == null) { + index = pref.findIndexOfValue(pref.getValue()); + } else { + index = pref.findIndexOfValue(overrideValue); + if (index == -1) { + // Avoid the crash if camera driver has bugs. + Log.e(TAG, "Fail to find override value=" + overrideValue); + pref.print(); + return; + } + } + item.setImageResource(mActivity, iconIds[index]); + } else { + // The preference only has a single icon to represent it. + item.setImageResource(mActivity, pref.getSingleIcon()); + } + } + + // Scene mode may override other camera settings (ex: flash mode). + public void overrideSettings(final String ... keyvalues) { + if (keyvalues.length % 2 != 0) { + throw new IllegalArgumentException(); + } + for (IconListPreference pref : mPreferenceMap.keySet()) { + override(pref, keyvalues); + } + } + + private void override(IconListPreference pref, final String ... keyvalues) { + mOverrides.remove(pref); + for (int i = 0; i < keyvalues.length; i += 2) { + String key = keyvalues[i]; + String value = keyvalues[i + 1]; + if (key.equals(pref.getKey())) { + mOverrides.put(pref, value); + PieItem item = mPreferenceMap.get(pref); + item.setEnabled(value == null); + break; + } + } + reloadPreference(pref); + } +} diff --git a/src/com/android/camera/PreferenceGroup.java b/src/com/android/camera/PreferenceGroup.java new file mode 100644 index 000000000..4d0519f4e --- /dev/null +++ b/src/com/android/camera/PreferenceGroup.java @@ -0,0 +1,79 @@ +/* + * 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.content.Context; +import android.util.AttributeSet; + +import java.util.ArrayList; + +/** + * A collection of CameraPreferences. It may contain other + * PreferenceGroup and form a tree structure. + */ +public class PreferenceGroup extends CameraPreference { + private ArrayList list = + new ArrayList(); + + public PreferenceGroup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void addChild(CameraPreference child) { + list.add(child); + } + + public void removePreference(int index) { + list.remove(index); + } + + public CameraPreference get(int index) { + return list.get(index); + } + + public int size() { + return list.size(); + } + + @Override + public void reloadValue() { + for (CameraPreference pref : list) { + pref.reloadValue(); + } + } + + /** + * Finds the preference with the given key recursively. Returns + * null if cannot find. + */ + public ListPreference findPreference(String key) { + // Find a leaf preference with the given key. Currently, the base + // type of all "leaf" preference is "ListPreference". If we add some + // other types later, we need to change the code. + for (CameraPreference pref : list) { + if (pref instanceof ListPreference) { + ListPreference listPref = (ListPreference) pref; + if(listPref.getKey().equals(key)) return listPref; + } else if(pref instanceof PreferenceGroup) { + ListPreference listPref = + ((PreferenceGroup) pref).findPreference(key); + if (listPref != null) return listPref; + } + } + return null; + } +} diff --git a/src/com/android/camera/PreferenceInflater.java b/src/com/android/camera/PreferenceInflater.java new file mode 100644 index 000000000..231c9833b --- /dev/null +++ b/src/com/android/camera/PreferenceInflater.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.InflateException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Inflate CameraPreference from XML resource. + */ +public class PreferenceInflater { + private static final String PACKAGE_NAME = + PreferenceInflater.class.getPackage().getName(); + + private static final Class[] CTOR_SIGNATURE = + new Class[] {Context.class, AttributeSet.class}; + private static final HashMap> sConstructorMap = + new HashMap>(); + + private Context mContext; + + public PreferenceInflater(Context context) { + mContext = context; + } + + public CameraPreference inflate(int resId) { + return inflate(mContext.getResources().getXml(resId)); + } + + private CameraPreference newPreference(String tagName, Object[] args) { + String name = PACKAGE_NAME + "." + tagName; + Constructor constructor = sConstructorMap.get(name); + try { + if (constructor == null) { + // Class not found in the cache, see if it's real, and try to + // add it + Class clazz = mContext.getClassLoader().loadClass(name); + constructor = clazz.getConstructor(CTOR_SIGNATURE); + sConstructorMap.put(name, constructor); + } + return (CameraPreference) constructor.newInstance(args); + } catch (NoSuchMethodException e) { + throw new InflateException("Error inflating class " + name, e); + } catch (ClassNotFoundException e) { + throw new InflateException("No such class: " + name, e); + } catch (Exception e) { + throw new InflateException("While create instance of" + name, e); + } + } + + private CameraPreference inflate(XmlPullParser parser) { + + AttributeSet attrs = Xml.asAttributeSet(parser); + ArrayList list = new ArrayList(); + Object args[] = new Object[]{mContext, attrs}; + + try { + for (int type = parser.next(); + type != XmlPullParser.END_DOCUMENT; type = parser.next()) { + if (type != XmlPullParser.START_TAG) continue; + CameraPreference pref = newPreference(parser.getName(), args); + + int depth = parser.getDepth(); + if (depth > list.size()) { + list.add(pref); + } else { + list.set(depth - 1, pref); + } + if (depth > 1) { + ((PreferenceGroup) list.get(depth - 2)).addChild(pref); + } + } + + if (list.size() == 0) { + throw new InflateException("No root element found"); + } + return list.get(0); + } catch (XmlPullParserException e) { + throw new InflateException(e); + } catch (IOException e) { + throw new InflateException(parser.getPositionDescription(), e); + } + } +} diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java new file mode 100644 index 000000000..451a35ad3 --- /dev/null +++ b/src/com/android/camera/PreviewFrameLayout.java @@ -0,0 +1,144 @@ +/* + * 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.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewStub; +import android.widget.RelativeLayout; + +import com.android.camera.ui.LayoutChangeHelper; +import com.android.camera.ui.LayoutChangeNotifier; +import com.android.gallery3d.common.ApiHelper; + +/** + * 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); + if (ApiHelper.HAS_FACE_DETECTION) { + ViewStub faceViewStub = (ViewStub) findViewById(R.id.face_view_stub); + /* preview_frame_video.xml does not have face view stub, so we need to + * check that. + */ + if (faceViewStub != null) { + faceViewStub.inflate(); + } + } + } + + 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() { + Util.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); + } + + @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/PreviewGestures.java b/src/com/android/camera/PreviewGestures.java new file mode 100644 index 000000000..2dccc3e45 --- /dev/null +++ b/src/com/android/camera/PreviewGestures.java @@ -0,0 +1,329 @@ +/* + * 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; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.ViewConfiguration; + +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.ZoomRenderer; + +import java.util.ArrayList; +import java.util.List; + +public class PreviewGestures + implements ScaleGestureDetector.OnScaleGestureListener { + + private static final String TAG = "CAM_gestures"; + + private static final long TIMEOUT_PIE = 200; + private static final int MSG_PIE = 1; + private static final int MODE_NONE = 0; + private static final int MODE_PIE = 1; + private static final int MODE_ZOOM = 2; + private static final int MODE_MODULE = 3; + private static final int MODE_ALL = 4; + + private CameraActivity mActivity; + private CameraModule mModule; + private RenderOverlay mOverlay; + private PieRenderer mPie; + private ZoomRenderer mZoom; + private MotionEvent mDown; + private MotionEvent mCurrent; + private ScaleGestureDetector mScale; + private List mReceivers; + private int mMode; + private int mSlop; + private int mTapTimeout; + private boolean mEnabled; + private boolean mZoomOnly; + private int mOrientation; + private int[] mLocation; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + if (msg.what == MSG_PIE) { + mMode = MODE_PIE; + openPie(); + cancelActivityTouchHandling(mDown); + } + } + }; + + public PreviewGestures(CameraActivity ctx, CameraModule module, + ZoomRenderer zoom, PieRenderer pie) { + mActivity = ctx; + mModule = module; + mPie = pie; + mZoom = zoom; + mMode = MODE_ALL; + mScale = new ScaleGestureDetector(ctx, this); + mSlop = (int) ctx.getResources().getDimension(R.dimen.pie_touch_slop); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mEnabled = true; + mLocation = new int[2]; + } + + public void setRenderOverlay(RenderOverlay overlay) { + mOverlay = overlay; + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (!enabled) { + cancelPie(); + } + } + + public void setZoomOnly(boolean zoom) { + mZoomOnly = zoom; + } + + public void addTouchReceiver(View v) { + if (mReceivers == null) { + mReceivers = new ArrayList(); + } + mReceivers.add(v); + } + + public void clearTouchReceivers() { + if (mReceivers != null) { + mReceivers.clear(); + } + } + + public boolean dispatchTouch(MotionEvent m) { + if (!mEnabled) { + return mActivity.superDispatchTouchEvent(m); + } + mCurrent = m; + if (MotionEvent.ACTION_DOWN == m.getActionMasked()) { + if (checkReceivers(m)) { + mMode = MODE_MODULE; + return mActivity.superDispatchTouchEvent(m); + } else { + mMode = MODE_ALL; + mDown = MotionEvent.obtain(m); + if (mPie != null && mPie.showsItems()) { + mMode = MODE_PIE; + return sendToPie(m); + } + if (mPie != null && !mZoomOnly) { + mHandler.sendEmptyMessageDelayed(MSG_PIE, TIMEOUT_PIE); + } + if (mZoom != null) { + mScale.onTouchEvent(m); + } + // make sure this is ok + return mActivity.superDispatchTouchEvent(m); + } + } else if (mMode == MODE_NONE) { + return false; + } else if (mMode == MODE_PIE) { + if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) { + sendToPie(makeCancelEvent(m)); + if (mZoom != null) { + onScaleBegin(mScale); + } + } else { + return sendToPie(m); + } + return true; + } else if (mMode == MODE_ZOOM) { + mScale.onTouchEvent(m); + if (!mScale.isInProgress() && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) { + mMode = MODE_NONE; + onScaleEnd(mScale); + } + return true; + } else if (mMode == MODE_MODULE) { + return mActivity.superDispatchTouchEvent(m); + } else { + // didn't receive down event previously; + // assume module wasn't initialzed and ignore this event. + if (mDown == null) { + return true; + } + if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) { + if (!mZoomOnly) { + cancelPie(); + sendToPie(makeCancelEvent(m)); + } + if (mZoom != null) { + mScale.onTouchEvent(m); + onScaleBegin(mScale); + } + } else if ((mMode == MODE_ZOOM) && !mScale.isInProgress() + && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) { + // user initiated and stopped zoom gesture without zooming + mScale.onTouchEvent(m); + onScaleEnd(mScale); + } + // not zoom or pie mode and no timeout yet + if (mZoom != null) { + boolean res = mScale.onTouchEvent(m); + if (mScale.isInProgress()) { + cancelPie(); + cancelActivityTouchHandling(m); + return res; + } + } + if (MotionEvent.ACTION_UP == m.getActionMasked()) { + cancelPie(); + cancelActivityTouchHandling(m); + // must have been tap + if (m.getEventTime() - mDown.getEventTime() < mTapTimeout) { + mModule.onSingleTapUp(null, + (int) mDown.getX() - mOverlay.getWindowPositionX(), + (int) mDown.getY() - mOverlay.getWindowPositionY()); + return true; + } else { + return mActivity.superDispatchTouchEvent(m); + } + } else if (MotionEvent.ACTION_MOVE == m.getActionMasked()) { + if ((Math.abs(m.getX() - mDown.getX()) > mSlop) + || Math.abs(m.getY() - mDown.getY()) > mSlop) { + // moved too far and no timeout yet, no focus or pie + cancelPie(); + if (isSwipe(m, true)) { + mMode = MODE_MODULE; + return mActivity.superDispatchTouchEvent(m); + } else { + cancelActivityTouchHandling(m); + if (isSwipe(m , false)) { + mMode = MODE_NONE; + } else if (!mZoomOnly) { + mMode = MODE_PIE; + openPie(); + sendToPie(m); + } + } + } + } + return false; + } + } + + private boolean checkReceivers(MotionEvent m) { + if (mReceivers != null) { + for (View receiver : mReceivers) { + if (isInside(m, receiver)) { + return true; + } + } + } + return false; + } + + // left tests for finger moving right to left + private boolean isSwipe(MotionEvent m, boolean left) { + float dx = 0; + float dy = 0; + switch (mOrientation) { + case 0: + dx = m.getX() - mDown.getX(); + dy = Math.abs(m.getY() - mDown.getY()); + break; + case 90: + dx = - (m.getY() - mDown.getY()); + dy = Math.abs(m.getX() - mDown.getX()); + break; + case 180: + dx = -(m.getX() - mDown.getX()); + dy = Math.abs(m.getY() - mDown.getY()); + break; + case 270: + dx = m.getY() - mDown.getY(); + dy = Math.abs(m.getX() - mDown.getX()); + break; + } + if (left) { + return (dx < 0 && dy / -dx < 0.6f); + } else { + return (dx > 0 && dy / dx < 0.6f); + } + } + + private boolean isInside(MotionEvent evt, View v) { + v.getLocationInWindow(mLocation); + return (v.getVisibility() == View.VISIBLE + && evt.getX() >= mLocation[0] && evt.getX() < mLocation[0] + v.getWidth() + && evt.getY() >= mLocation[1] && evt.getY() < mLocation[1] + v.getHeight()); + } + + public void cancelActivityTouchHandling(MotionEvent m) { + mActivity.superDispatchTouchEvent(makeCancelEvent(m)); + } + + private MotionEvent makeCancelEvent(MotionEvent m) { + MotionEvent c = MotionEvent.obtain(m); + c.setAction(MotionEvent.ACTION_CANCEL); + return c; + } + + private void openPie() { + mDown.offsetLocation(-mOverlay.getWindowPositionX(), + -mOverlay.getWindowPositionY()); + mOverlay.directDispatchTouch(mDown, mPie); + } + + private void cancelPie() { + mHandler.removeMessages(MSG_PIE); + } + + private boolean sendToPie(MotionEvent m) { + m.offsetLocation(-mOverlay.getWindowPositionX(), + -mOverlay.getWindowPositionY()); + return mOverlay.directDispatchTouch(m, mPie); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mZoom.onScale(detector); + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (mMode != MODE_ZOOM) { + mMode = MODE_ZOOM; + cancelActivityTouchHandling(mCurrent); + } + if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) { + return mZoom.onScaleBegin(detector); + } else { + return true; + } + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) { + mZoom.onScaleEnd(detector); + } + } +} diff --git a/src/com/android/camera/ProxyLauncher.java b/src/com/android/camera/ProxyLauncher.java new file mode 100644 index 000000000..8c566214c --- /dev/null +++ b/src/com/android/camera/ProxyLauncher.java @@ -0,0 +1,46 @@ +/* + * 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; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class ProxyLauncher extends Activity { + + public static final int RESULT_USER_CANCELED = -2; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT); + startActivityForResult(intent, 0); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_CANCELED) { + resultCode = RESULT_USER_CANCELED; + } + setResult(resultCode, data); + finish(); + } + +} diff --git a/src/com/android/camera/RecordLocationPreference.java b/src/com/android/camera/RecordLocationPreference.java new file mode 100644 index 000000000..9992afabb --- /dev/null +++ b/src/com/android/camera/RecordLocationPreference.java @@ -0,0 +1,58 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.AttributeSet; + +/** + * {@code RecordLocationPreference} is used to keep the "store locaiton" + * option in {@code SharedPreference}. + */ +public class RecordLocationPreference extends IconListPreference { + + public static final String VALUE_NONE = "none"; + public static final String VALUE_ON = "on"; + public static final String VALUE_OFF = "off"; + + private final ContentResolver mResolver; + + public RecordLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mResolver = context.getContentResolver(); + } + + @Override + public String getValue() { + return get(getSharedPreferences(), mResolver) ? VALUE_ON : VALUE_OFF; + } + + public static boolean get( + SharedPreferences pref, ContentResolver resolver) { + String value = pref.getString( + CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE); + return VALUE_ON.equals(value); + } + + public static boolean isSet(SharedPreferences pref) { + String value = pref.getString( + CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE); + return !VALUE_NONE.equals(value); + } +} diff --git a/src/com/android/camera/RotateDialogController.java b/src/com/android/camera/RotateDialogController.java new file mode 100644 index 000000000..700d35434 --- /dev/null +++ b/src/com/android/camera/RotateDialogController.java @@ -0,0 +1,168 @@ +/* + * 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.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.camera.ui.Rotatable; +import com.android.camera.ui.RotateLayout; + +public class RotateDialogController implements Rotatable { + + @SuppressWarnings("unused") + private static final String TAG = "RotateDialogController"; + private static final long ANIM_DURATION = 150; // millis + + private Activity mActivity; + private int mLayoutResourceID; + private View mDialogRootLayout; + private RotateLayout mRotateDialog; + private View mRotateDialogTitleLayout; + private View mRotateDialogButtonLayout; + private TextView mRotateDialogTitle; + private ProgressBar mRotateDialogSpinner; + private TextView mRotateDialogText; + private TextView mRotateDialogButton1; + private TextView mRotateDialogButton2; + + private Animation mFadeInAnim, mFadeOutAnim; + + public RotateDialogController(Activity a, int layoutResource) { + mActivity = a; + mLayoutResourceID = layoutResource; + } + + private void inflateDialogLayout() { + if (mDialogRootLayout == null) { + ViewGroup layoutRoot = (ViewGroup) mActivity.getWindow().getDecorView(); + LayoutInflater inflater = mActivity.getLayoutInflater(); + View v = inflater.inflate(mLayoutResourceID, layoutRoot); + mDialogRootLayout = v.findViewById(R.id.rotate_dialog_root_layout); + mRotateDialog = (RotateLayout) v.findViewById(R.id.rotate_dialog_layout); + mRotateDialogTitleLayout = v.findViewById(R.id.rotate_dialog_title_layout); + mRotateDialogButtonLayout = v.findViewById(R.id.rotate_dialog_button_layout); + mRotateDialogTitle = (TextView) v.findViewById(R.id.rotate_dialog_title); + mRotateDialogSpinner = (ProgressBar) v.findViewById(R.id.rotate_dialog_spinner); + mRotateDialogText = (TextView) v.findViewById(R.id.rotate_dialog_text); + mRotateDialogButton1 = (Button) v.findViewById(R.id.rotate_dialog_button1); + mRotateDialogButton2 = (Button) v.findViewById(R.id.rotate_dialog_button2); + + mFadeInAnim = AnimationUtils.loadAnimation( + mActivity, android.R.anim.fade_in); + mFadeOutAnim = AnimationUtils.loadAnimation( + mActivity, android.R.anim.fade_out); + mFadeInAnim.setDuration(ANIM_DURATION); + mFadeOutAnim.setDuration(ANIM_DURATION); + } + } + + @Override + public void setOrientation(int orientation, boolean animation) { + inflateDialogLayout(); + mRotateDialog.setOrientation(orientation, animation); + } + + public void resetRotateDialog() { + inflateDialogLayout(); + mRotateDialogTitleLayout.setVisibility(View.GONE); + mRotateDialogSpinner.setVisibility(View.GONE); + mRotateDialogButton1.setVisibility(View.GONE); + mRotateDialogButton2.setVisibility(View.GONE); + mRotateDialogButtonLayout.setVisibility(View.GONE); + } + + private void fadeOutDialog() { + mDialogRootLayout.startAnimation(mFadeOutAnim); + mDialogRootLayout.setVisibility(View.GONE); + } + + private void fadeInDialog() { + mDialogRootLayout.startAnimation(mFadeInAnim); + mDialogRootLayout.setVisibility(View.VISIBLE); + } + + public void dismissDialog() { + if (mDialogRootLayout != null && mDialogRootLayout.getVisibility() != View.GONE) { + fadeOutDialog(); + } + } + + public void showAlertDialog(String title, String msg, String button1Text, + final Runnable r1, String button2Text, final Runnable r2) { + resetRotateDialog(); + + if (title != null) { + mRotateDialogTitle.setText(title); + mRotateDialogTitleLayout.setVisibility(View.VISIBLE); + } + + mRotateDialogText.setText(msg); + + if (button1Text != null) { + mRotateDialogButton1.setText(button1Text); + mRotateDialogButton1.setContentDescription(button1Text); + mRotateDialogButton1.setVisibility(View.VISIBLE); + mRotateDialogButton1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (r1 != null) r1.run(); + dismissDialog(); + } + }); + mRotateDialogButtonLayout.setVisibility(View.VISIBLE); + } + if (button2Text != null) { + mRotateDialogButton2.setText(button2Text); + mRotateDialogButton2.setContentDescription(button2Text); + mRotateDialogButton2.setVisibility(View.VISIBLE); + mRotateDialogButton2.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (r2 != null) r2.run(); + dismissDialog(); + } + }); + mRotateDialogButtonLayout.setVisibility(View.VISIBLE); + } + + fadeInDialog(); + } + + public void showWaitingDialog(String msg) { + resetRotateDialog(); + + mRotateDialogText.setText(msg); + mRotateDialogSpinner.setVisibility(View.VISIBLE); + + fadeInDialog(); + } + + public int getVisibility() { + if (mDialogRootLayout != null) { + return mDialogRootLayout.getVisibility(); + } + return View.INVISIBLE; + } +} diff --git a/src/com/android/camera/SecureCameraActivity.java b/src/com/android/camera/SecureCameraActivity.java new file mode 100644 index 000000000..2fa68f8e6 --- /dev/null +++ b/src/com/android/camera/SecureCameraActivity.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +// Use a different activity for secure camera only. So it can have a different +// task affinity from others. This makes sure non-secure camera activity is not +// started in secure lock screen. +public class SecureCameraActivity extends CameraActivity { +} diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java new file mode 100755 index 000000000..a1bbb1a0d --- /dev/null +++ b/src/com/android/camera/ShutterButton.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2008 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.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; + +/** + * A button designed to be used for the on-screen shutter button. + * It's currently an {@code ImageView} that can call a delegate when the + * pressed state changes. + */ +public class ShutterButton extends ImageView { + + private boolean mTouchEnabled = true; + + /** + * A callback to be invoked when a ShutterButton's pressed state changes. + */ + public interface OnShutterButtonListener { + /** + * Called when a ShutterButton has been pressed. + * + * @param pressed The ShutterButton that was pressed. + */ + void onShutterButtonFocus(boolean pressed); + void onShutterButtonClick(); + } + + private OnShutterButtonListener mListener; + private boolean mOldPressed; + + public ShutterButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setOnShutterButtonListener(OnShutterButtonListener listener) { + mListener = listener; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mTouchEnabled) { + return super.dispatchTouchEvent(m); + } else { + return false; + } + } + + public void enableTouch(boolean enable) { + mTouchEnabled = enable; + } + + /** + * Hook into the drawable state changing to get changes to isPressed -- the + * onPressed listener doesn't always get called when the pressed state + * changes. + */ + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final boolean pressed = isPressed(); + if (pressed != mOldPressed) { + if (!pressed) { + // When pressing the physical camera button the sequence of + // events is: + // focus pressed, optional camera pressed, focus released. + // We want to emulate this sequence of events with the shutter + // button. When clicking using a trackball button, the view + // system changes the drawable state before posting click + // notification, so the sequence of events is: + // pressed(true), optional click, pressed(false) + // When clicking using touch events, the view system changes the + // drawable state after posting click notification, so the + // sequence of events is: + // pressed(true), pressed(false), optional click + // Since we're emulating the physical camera button, we want to + // have the same order of events. So we want the optional click + // callback to be delivered before the pressed(false) callback. + // + // To do this, we delay the posting of the pressed(false) event + // slightly by pushing it on the event queue. This moves it + // after the optional click notification, so our client always + // sees events in this sequence: + // pressed(true), optional click, pressed(false) + post(new Runnable() { + @Override + public void run() { + callShutterButtonFocus(pressed); + } + }); + } else { + callShutterButtonFocus(pressed); + } + mOldPressed = pressed; + } + } + + private void callShutterButtonFocus(boolean pressed) { + if (mListener != null) { + mListener.onShutterButtonFocus(pressed); + } + } + + @Override + public boolean performClick() { + boolean result = super.performClick(); + if (mListener != null && getVisibility() == View.VISIBLE) { + mListener.onShutterButtonClick(); + } + return result; + } +} diff --git a/src/com/android/camera/SoundClips.java b/src/com/android/camera/SoundClips.java new file mode 100644 index 000000000..b5e783105 --- /dev/null +++ b/src/com/android/camera/SoundClips.java @@ -0,0 +1,193 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaActionSound; +import android.media.SoundPool; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +/* + * This class controls the sound playback according to the API level. + */ +public class SoundClips { + // Sound actions. + public static final int FOCUS_COMPLETE = 0; + public static final int START_VIDEO_RECORDING = 1; + public static final int STOP_VIDEO_RECORDING = 2; + + public interface Player { + public void release(); + public void play(int action); + } + + public static Player getPlayer(Context context) { + if (ApiHelper.HAS_MEDIA_ACTION_SOUND) { + return new MediaActionSoundPlayer(); + } else { + return new SoundPoolPlayer(context); + } + } + + /** + * This class implements SoundClips.Player using MediaActionSound, + * which exists since API level 16. + */ + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private static class MediaActionSoundPlayer implements Player { + private static final String TAG = "MediaActionSoundPlayer"; + private MediaActionSound mSound; + + @Override + public void release() { + if (mSound != null) { + mSound.release(); + mSound = null; + } + } + + public MediaActionSoundPlayer() { + mSound = new MediaActionSound(); + mSound.load(MediaActionSound.START_VIDEO_RECORDING); + mSound.load(MediaActionSound.STOP_VIDEO_RECORDING); + mSound.load(MediaActionSound.FOCUS_COMPLETE); + } + + @Override + public synchronized void play(int action) { + switch(action) { + case FOCUS_COMPLETE: + mSound.play(MediaActionSound.FOCUS_COMPLETE); + break; + case START_VIDEO_RECORDING: + mSound.play(MediaActionSound.START_VIDEO_RECORDING); + break; + case STOP_VIDEO_RECORDING: + mSound.play(MediaActionSound.STOP_VIDEO_RECORDING); + break; + default: + Log.w(TAG, "Unrecognized action:" + action); + } + } + } + + /** + * This class implements SoundClips.Player using SoundPool, which + * exists since API level 1. + */ + private static class SoundPoolPlayer implements + Player, SoundPool.OnLoadCompleteListener { + + private static final String TAG = "SoundPoolPlayer"; + private static final int NUM_SOUND_STREAMS = 1; + private static final int[] SOUND_RES = { // Soundtrack res IDs. + R.raw.focus_complete, + R.raw.video_record + }; + + // ID returned by load() should be non-zero. + private static final int ID_NOT_LOADED = 0; + + // Maps a sound action to the id; + private final int[] mSoundRes = {0, 1, 1}; + // Store the context for lazy loading. + private Context mContext; + // mSoundPool is created every time load() is called and cleared every + // time release() is called. + private SoundPool mSoundPool; + // Sound ID of each sound resources. Given when the sound is loaded. + private final int[] mSoundIDs; + private final boolean[] mSoundIDReady; + private int mSoundIDToPlay; + + public SoundPoolPlayer(Context context) { + mContext = context; + int audioType = ApiHelper.getIntFieldIfExists(AudioManager.class, + "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING); + + mSoundIDToPlay = ID_NOT_LOADED; + + mSoundPool = new SoundPool(NUM_SOUND_STREAMS, audioType, 0); + mSoundPool.setOnLoadCompleteListener(this); + + mSoundIDs = new int[SOUND_RES.length]; + mSoundIDReady = new boolean[SOUND_RES.length]; + for (int i = 0; i < SOUND_RES.length; i++) { + mSoundIDs[i] = mSoundPool.load(mContext, SOUND_RES[i], 1); + mSoundIDReady[i] = false; + } + } + + @Override + public synchronized void release() { + if (mSoundPool != null) { + mSoundPool.release(); + mSoundPool = null; + } + } + + @Override + public synchronized void play(int action) { + if (action < 0 || action >= mSoundRes.length) { + Log.e(TAG, "Resource ID not found for action:" + action + " in play()."); + return; + } + + int index = mSoundRes[action]; + if (mSoundIDs[index] == ID_NOT_LOADED) { + // Not loaded yet, load first and then play when the loading is complete. + mSoundIDs[index] = mSoundPool.load(mContext, SOUND_RES[index], 1); + mSoundIDToPlay = mSoundIDs[index]; + } else if (!mSoundIDReady[index]) { + // Loading and not ready yet. + mSoundIDToPlay = mSoundIDs[index]; + } else { + mSoundPool.play(mSoundIDs[index], 1f, 1f, 0, 0, 1f); + } + } + + @Override + public void onLoadComplete(SoundPool pool, int soundID, int status) { + if (status != 0) { + Log.e(TAG, "loading sound tracks failed (status=" + status + ")"); + for (int i = 0; i < mSoundIDs.length; i++ ) { + if (mSoundIDs[i] == soundID) { + mSoundIDs[i] = ID_NOT_LOADED; + break; + } + } + return; + } + + for (int i = 0; i < mSoundIDs.length; i++ ) { + if (mSoundIDs[i] == soundID) { + mSoundIDReady[i] = true; + break; + } + } + + if (soundID == mSoundIDToPlay) { + mSoundIDToPlay = ID_NOT_LOADED; + mSoundPool.play(soundID, 1f, 1f, 0, 0, 1f); + } + } + } +} diff --git a/src/com/android/camera/StaticBitmapScreenNail.java b/src/com/android/camera/StaticBitmapScreenNail.java new file mode 100644 index 000000000..10788c0fb --- /dev/null +++ b/src/com/android/camera/StaticBitmapScreenNail.java @@ -0,0 +1,32 @@ +/* + * 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; + +import android.graphics.Bitmap; + +import com.android.gallery3d.ui.BitmapScreenNail; + +public class StaticBitmapScreenNail extends BitmapScreenNail { + public StaticBitmapScreenNail(Bitmap bitmap) { + super(bitmap); + } + + @Override + public void recycle() { + // Always keep the bitmap in memory. + } +} diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java new file mode 100644 index 000000000..648fa7d87 --- /dev/null +++ b/src/com/android/camera/Storage.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.MediaColumns; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.File; +import java.io.FileOutputStream; + +public class Storage { + private static final String TAG = "CameraStorage"; + + public static final String DCIM = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); + + public static final String DIRECTORY = DCIM + "/Camera"; + + // Match the code in MediaProvider.computeBucketValues(). + public static final String BUCKET_ID = + String.valueOf(DIRECTORY.toLowerCase().hashCode()); + + public static final long UNAVAILABLE = -1L; + public static final long PREPARING = -2L; + public static final long UNKNOWN_SIZE = -3L; + public static final long LOW_STORAGE_THRESHOLD= 50000000; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static void setImageSize(ContentValues values, int width, int height) { + // The two fields are available since ICS but got published in JB + if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { + values.put(MediaColumns.WIDTH, width); + values.put(MediaColumns.HEIGHT, height); + } + } + + public static void writeFile(String path, byte[] data) { + FileOutputStream out = null; + try { + out = new FileOutputStream(path); + out.write(data); + } catch (Exception e) { + Log.e(TAG, "Failed to write data", e); + } finally { + try { + out.close(); + } catch (Exception e) { + } + } + } + + // Save the image and add it to media store. + public static Uri addImage(ContentResolver resolver, String title, + long date, Location location, int orientation, byte[] jpeg, + int width, int height) { + // Save the image. + String path = generateFilepath(title); + writeFile(path, jpeg); + return addImage(resolver, title, date, location, orientation, + jpeg.length, path, width, height); + } + + // Add the image to media store. + public static Uri addImage(ContentResolver resolver, String title, + long date, Location location, int orientation, int jpegLength, + String path, int width, int height) { + // Insert into MediaStore. + ContentValues values = new ContentValues(9); + values.put(ImageColumns.TITLE, title); + values.put(ImageColumns.DISPLAY_NAME, title + ".jpg"); + values.put(ImageColumns.DATE_TAKEN, date); + values.put(ImageColumns.MIME_TYPE, "image/jpeg"); + // Clockwise rotation in degrees. 0, 90, 180, or 270. + values.put(ImageColumns.ORIENTATION, orientation); + values.put(ImageColumns.DATA, path); + values.put(ImageColumns.SIZE, jpegLength); + + setImageSize(values, width, height); + + if (location != null) { + values.put(ImageColumns.LATITUDE, location.getLatitude()); + values.put(ImageColumns.LONGITUDE, location.getLongitude()); + } + + Uri uri = null; + try { + uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); + } catch (Throwable th) { + // This can happen when the external volume is already mounted, but + // MediaScanner has not notify MediaProvider to add that volume. + // The picture is still safe and MediaScanner will find it and + // insert it into MediaProvider. The only problem is that the user + // cannot click the thumbnail to review the picture. + Log.e(TAG, "Failed to write MediaStore" + th); + } + return uri; + } + + public static void deleteImage(ContentResolver resolver, Uri uri) { + try { + resolver.delete(uri, null, null); + } catch (Throwable th) { + Log.e(TAG, "Failed to delete image: " + uri); + } + } + + public static String generateFilepath(String title) { + return DIRECTORY + '/' + title + ".jpg"; + } + + public static long getAvailableSpace() { + String state = Environment.getExternalStorageState(); + Log.d(TAG, "External storage state=" + state); + if (Environment.MEDIA_CHECKING.equals(state)) { + return PREPARING; + } + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return UNAVAILABLE; + } + + File dir = new File(DIRECTORY); + dir.mkdirs(); + if (!dir.isDirectory() || !dir.canWrite()) { + return UNAVAILABLE; + } + + try { + StatFs stat = new StatFs(DIRECTORY); + return stat.getAvailableBlocks() * (long) stat.getBlockSize(); + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return UNKNOWN_SIZE; + } + + /** + * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be + * imported. This is a temporary fix for bug#1655552. + */ + public static void ensureOSXCompatible() { + File nnnAAAAA = new File(DCIM, "100ANDRO"); + if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { + Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); + } + } +} diff --git a/src/com/android/camera/SwitchAnimManager.java b/src/com/android/camera/SwitchAnimManager.java new file mode 100644 index 000000000..6ec88223e --- /dev/null +++ b/src/com/android/camera/SwitchAnimManager.java @@ -0,0 +1,146 @@ +/* + * 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; + +import android.os.SystemClock; +import android.util.Log; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; + +/** + * Class to handle the animation when switching between back and front cameras. + * An image of the previous camera zooms in and fades out. The preview of the + * new camera zooms in and fades in. The image of the previous camera is called + * review in this class. + */ +public class SwitchAnimManager { + private static final String TAG = "SwitchAnimManager"; + // The amount of change for zooming in and out. + private static final float ZOOM_DELTA_PREVIEW = 0.2f; + private static final float ZOOM_DELTA_REVIEW = 0.5f; + private static final float ANIMATION_DURATION = 400; // ms + public static final float INITIAL_DARKEN_ALPHA = 0.8f; + + private long mAnimStartTime; // milliseconds. + // The drawing width and height of the review image. This is saved when the + // texture is copied. + private int mReviewDrawingWidth; + private int mReviewDrawingHeight; + // The maximum width of the camera screen nail width from onDraw. We need to + // know how much the preview is scaled and scale the review the same amount. + // For example, the preview is not full screen in film strip mode. + private int mPreviewFrameLayoutWidth; + + public SwitchAnimManager() { + } + + public void setReviewDrawingSize(int width, int height) { + mReviewDrawingWidth = width; + mReviewDrawingHeight = height; + } + + // width: the width of PreviewFrameLayout view. + // height: the height of PreviewFrameLayout view. Not used. Kept for + // consistency. + public void setPreviewFrameLayoutSize(int width, int height) { + mPreviewFrameLayoutWidth = width; + } + + // w and h: the rectangle area where the animation takes place. + public void startAnimation() { + mAnimStartTime = SystemClock.uptimeMillis(); + } + + // Returns true if the animation has been drawn. + // preview: camera preview view. + // review: snapshot of the preview before switching the camera. + public boolean drawAnimation(GLCanvas canvas, int x, int y, int width, + int height, CameraScreenNail preview, RawTexture review) { + long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime; + if (timeDiff > ANIMATION_DURATION) return false; + float fraction = timeDiff / ANIMATION_DURATION; + + // Calculate the position and the size of the preview. + float centerX = x + width / 2f; + float centerY = y + height / 2f; + float previewAnimScale = 1 - ZOOM_DELTA_PREVIEW * (1 - fraction); + float previewWidth = width * previewAnimScale; + float previewHeight = height * previewAnimScale; + int previewX = Math.round(centerX - previewWidth / 2); + int previewY = Math.round(centerY - previewHeight / 2); + + // Calculate the position and the size of the review. + float reviewAnimScale = 1 + ZOOM_DELTA_REVIEW * fraction; + + // Calculate how much preview is scaled. + // The scaling is done by PhotoView in Gallery so we don't have the + // scaling information but only the width and the height passed to this + // method. The inference of the scale ratio is done by matching the + // current width and the original width we have at first when the camera + // layout is inflated. + float scaleRatio = 1; + if (mPreviewFrameLayoutWidth != 0) { + scaleRatio = (float) width / mPreviewFrameLayoutWidth; + } else { + Log.e(TAG, "mPreviewFrameLayoutWidth is 0."); + } + float reviewWidth = mReviewDrawingWidth * reviewAnimScale * scaleRatio; + float reviewHeight = mReviewDrawingHeight * reviewAnimScale * scaleRatio; + int reviewX = Math.round(centerX - reviewWidth / 2); + int reviewY = Math.round(centerY - reviewHeight / 2); + + // Draw the preview. + float alpha = canvas.getAlpha(); + canvas.setAlpha(fraction); // fade in + preview.directDraw(canvas, previewX, previewY, Math.round(previewWidth), + Math.round(previewHeight)); + + // Draw the review. + canvas.setAlpha((1f - fraction) * INITIAL_DARKEN_ALPHA); // fade out + review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth), + Math.round(reviewHeight)); + canvas.setAlpha(alpha); + return true; + } + + public boolean drawDarkPreview(GLCanvas canvas, int x, int y, int width, + int height, RawTexture review) { + // Calculate the position and the size. + float centerX = x + width / 2f; + float centerY = y + height / 2f; + float scaleRatio = 1; + if (mPreviewFrameLayoutWidth != 0) { + scaleRatio = (float) width / mPreviewFrameLayoutWidth; + } else { + Log.e(TAG, "mPreviewFrameLayoutWidth is 0."); + } + float reviewWidth = mReviewDrawingWidth * scaleRatio; + float reviewHeight = mReviewDrawingHeight * scaleRatio; + int reviewX = Math.round(centerX - reviewWidth / 2); + int reviewY = Math.round(centerY - reviewHeight / 2); + + // Draw the review. + float alpha = canvas.getAlpha(); + canvas.setAlpha(INITIAL_DARKEN_ALPHA); + review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth), + Math.round(reviewHeight)); + canvas.setAlpha(alpha); + return true; + } + +} diff --git a/src/com/android/camera/Thumbnail.java b/src/com/android/camera/Thumbnail.java new file mode 100644 index 000000000..5f8483d6c --- /dev/null +++ b/src/com/android/camera/Thumbnail.java @@ -0,0 +1,68 @@ +/* + * 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.graphics.Bitmap; +import android.media.MediaMetadataRetriever; + +import java.io.FileDescriptor; + +public class Thumbnail { + public static Bitmap createVideoThumbnailBitmap(FileDescriptor fd, int targetWidth) { + return createVideoThumbnailBitmap(null, fd, targetWidth); + } + + public static Bitmap createVideoThumbnailBitmap(String filePath, int targetWidth) { + return createVideoThumbnailBitmap(filePath, null, targetWidth); + } + + private static Bitmap createVideoThumbnailBitmap(String filePath, FileDescriptor fd, + int targetWidth) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + if (filePath != null) { + retriever.setDataSource(filePath); + } else { + retriever.setDataSource(fd); + } + bitmap = retriever.getFrameAtTime(-1); + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } + } + if (bitmap == null) return null; + + // Scale down the bitmap if it is bigger than we need. + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if (width > targetWidth) { + float scale = (float) targetWidth / width; + int w = Math.round(scale * width); + int h = Math.round(scale * height); + bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); + } + return bitmap; + } +} diff --git a/src/com/android/camera/Util.java b/src/com/android/camera/Util.java new file mode 100644 index 000000000..2953d6ae7 --- /dev/null +++ b/src/com/android/camera/Util.java @@ -0,0 +1,776 @@ +/* + * 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.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.admin.DevicePolicyManager; +import android.content.ActivityNotFoundException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.telephony.TelephonyManager; +import android.util.DisplayMetrics; +import android.util.FloatMath; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Collection of utility functions used in this package. + */ +public class Util { + private static final String TAG = "Util"; + + // Orientation hysteresis amount used in rounding, in degrees + public static final int ORIENTATION_HYSTERESIS = 5; + + public static final String REVIEW_ACTION = "com.android.camera.action.REVIEW"; + // See android.hardware.Camera.ACTION_NEW_PICTURE. + public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE"; + // See android.hardware.Camera.ACTION_NEW_VIDEO. + public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO"; + + // Fields from android.hardware.Camera.Parameters + public static final String FOCUS_MODE_CONTINUOUS_PICTURE = "continuous-picture"; + public static final String RECORDING_HINT = "recording-hint"; + private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported"; + private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported"; + private static final String VIDEO_SNAPSHOT_SUPPORTED = "video-snapshot-supported"; + public static final String SCENE_MODE_HDR = "hdr"; + public static final String TRUE = "true"; + public static final String FALSE = "false"; + + public static boolean isSupported(String value, List supported) { + return supported == null ? false : supported.indexOf(value) >= 0; + } + + public static boolean isAutoExposureLockSupported(Parameters params) { + return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED)); + } + + public static boolean isAutoWhiteBalanceLockSupported(Parameters params) { + return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED)); + } + + public static boolean isVideoSnapshotSupported(Parameters params) { + return TRUE.equals(params.get(VIDEO_SNAPSHOT_SUPPORTED)); + } + + public static boolean isCameraHdrSupported(Parameters params) { + List supported = params.getSupportedSceneModes(); + return (supported != null) && supported.contains(SCENE_MODE_HDR); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public static boolean isMeteringAreaSupported(Parameters params) { + if (ApiHelper.HAS_CAMERA_METERING_AREA) { + return params.getMaxNumMeteringAreas() > 0; + } + return false; + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public static boolean isFocusAreaSupported(Parameters params) { + if (ApiHelper.HAS_CAMERA_FOCUS_AREA) { + return (params.getMaxNumFocusAreas() > 0 + && isSupported(Parameters.FOCUS_MODE_AUTO, + params.getSupportedFocusModes())); + } + return false; + } + + // Private intent extras. Test only. + private static final String EXTRAS_CAMERA_FACING = + "android.intent.extras.CAMERA_FACING"; + + private static float sPixelDensity = 1; + private static ImageFileNamer sImageFileNamer; + + private Util() { + } + + public static void initialize(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sPixelDensity = metrics.density; + sImageFileNamer = new ImageFileNamer( + context.getString(R.string.image_file_name_format)); + } + + public static int dpToPixel(int dp) { + return Math.round(sPixelDensity * dp); + } + + // Rotates the bitmap by the specified degree. + // If a new bitmap is created, the original bitmap is recycled. + public static Bitmap rotate(Bitmap b, int degrees) { + return rotateAndMirror(b, degrees, false); + } + + // Rotates and/or mirrors the bitmap. If a new bitmap is created, the + // original bitmap is recycled. + public static Bitmap rotateAndMirror(Bitmap b, int degrees, boolean mirror) { + if ((degrees != 0 || mirror) && b != null) { + Matrix m = new Matrix(); + // Mirror first. + // horizontal flip + rotation = -rotation + horizontal flip + if (mirror) { + m.postScale(-1, 1); + degrees = (degrees + 360) % 360; + if (degrees == 0 || degrees == 180) { + m.postTranslate(b.getWidth(), 0); + } else if (degrees == 90 || degrees == 270) { + m.postTranslate(b.getHeight(), 0); + } else { + throw new IllegalArgumentException("Invalid degrees=" + degrees); + } + } + if (degrees != 0) { + // clockwise + m.postRotate(degrees, + (float) b.getWidth() / 2, (float) b.getHeight() / 2); + } + + try { + Bitmap b2 = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + if (b != b2) { + b.recycle(); + b = b2; + } + } catch (OutOfMemoryError ex) { + // We have no memory to rotate. Return the original bitmap. + } + } + return b; + } + + /* + * Compute the sample size as a function of minSideLength + * and maxNumOfPixels. + * minSideLength is used to specify that minimal width or height of a + * bitmap. + * maxNumOfPixels is used to specify the maximal size in pixels that is + * tolerable in terms of memory usage. + * + * The function returns a sample size based on the constraints. + * Both size and minSideLength can be passed in as -1 + * which indicates no care of the corresponding constraint. + * The functions prefers returning a sample size that + * generates a smaller bitmap, unless minSideLength = -1. + * + * Also, the function rounds up the sample size to a power of 2 or multiple + * of 8 because BitmapFactory only honors sample size this way. + * For example, BitmapFactory downsamples an image by 2 even though the + * request is 3. So we round up the sample size to avoid OOM. + */ + public static int computeSampleSize(BitmapFactory.Options options, + int minSideLength, int maxNumOfPixels) { + int initialSize = computeInitialSampleSize(options, minSideLength, + maxNumOfPixels); + + int roundedSize; + if (initialSize <= 8) { + roundedSize = 1; + while (roundedSize < initialSize) { + roundedSize <<= 1; + } + } else { + roundedSize = (initialSize + 7) / 8 * 8; + } + + return roundedSize; + } + + private static int computeInitialSampleSize(BitmapFactory.Options options, + int minSideLength, int maxNumOfPixels) { + double w = options.outWidth; + double h = options.outHeight; + + int lowerBound = (maxNumOfPixels < 0) ? 1 : + (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); + int upperBound = (minSideLength < 0) ? 128 : + (int) Math.min(Math.floor(w / minSideLength), + Math.floor(h / minSideLength)); + + if (upperBound < lowerBound) { + // return the larger one when there is no overlapping zone. + return lowerBound; + } + + if (maxNumOfPixels < 0 && minSideLength < 0) { + return 1; + } else if (minSideLength < 0) { + return lowerBound; + } else { + return upperBound; + } + } + + public static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, + options); + if (options.mCancel || options.outWidth == -1 + || options.outHeight == -1) { + return null; + } + options.inSampleSize = computeSampleSize( + options, -1, maxNumOfPixels); + options.inJustDecodeBounds = false; + + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, + options); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "Got oom exception ", ex); + return null; + } + } + + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + public static void Assert(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void throwIfCameraDisabled(Activity activity) throws CameraDisabledException { + // Check if device policy has disabled the camera. + if (ApiHelper.HAS_GET_CAMERA_DISABLED) { + DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService( + Context.DEVICE_POLICY_SERVICE); + if (dpm.getCameraDisabled(null)) { + throw new CameraDisabledException(); + } + } + } + + public static CameraManager.CameraProxy openCamera(Activity activity, int cameraId) + throws CameraHardwareException, CameraDisabledException { + throwIfCameraDisabled(activity); + + try { + return CameraHolder.instance().open(cameraId); + } catch (CameraHardwareException e) { + // In eng build, we throw the exception so that test tool + // can detect it and report it + if ("eng".equals(Build.TYPE)) { + throw new RuntimeException("openCamera failed", e); + } else { + throw e; + } + } + } + + public static void showErrorAndFinish(final Activity activity, int msgId) { + DialogInterface.OnClickListener buttonListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + activity.finish(); + } + }; + TypedValue out = new TypedValue(); + activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, out, true); + new AlertDialog.Builder(activity) + .setCancelable(false) + .setTitle(R.string.camera_error_title) + .setMessage(msgId) + .setNeutralButton(R.string.dialog_ok, buttonListener) + .setIcon(out.resourceId) + .show(); + } + + public static T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + public static boolean equals(Object a, Object b) { + return (a == b) || (a == null ? false : a.equals(b)); + } + + public static int nextPowerOf2(int n) { + n -= 1; + n |= n >>> 16; + n |= n >>> 8; + n |= n >>> 4; + n |= n >>> 2; + n |= n >>> 1; + return n + 1; + } + + public static float distance(float x, float y, float sx, float sy) { + float dx = x - sx; + float dy = y - sy; + return FloatMath.sqrt(dx * dx + dy * dy); + } + + public static int clamp(int x, int min, int max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + public static int getDisplayRotation(Activity activity) { + int rotation = activity.getWindowManager().getDefaultDisplay() + .getRotation(); + switch (rotation) { + case Surface.ROTATION_0: return 0; + case Surface.ROTATION_90: return 90; + case Surface.ROTATION_180: return 180; + case Surface.ROTATION_270: return 270; + } + return 0; + } + + public static int getDisplayOrientation(int degrees, int cameraId) { + // See android.hardware.Camera.setDisplayOrientation for + // documentation. + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + int result; + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + public static int getCameraOrientation(int cameraId) { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + return info.orientation; + } + + public static int roundOrientation(int orientation, int orientationHistory) { + boolean changeOrientation = false; + if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) { + changeOrientation = true; + } else { + int dist = Math.abs(orientation - orientationHistory); + dist = Math.min( dist, 360 - dist ); + changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS ); + } + if (changeOrientation) { + return ((orientation + 45) / 90 * 90) % 360; + } + return orientationHistory; + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + private static Point getDefaultDisplaySize(Activity activity, Point size) { + Display d = activity.getWindowManager().getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) { + d.getSize(size); + } else { + size.set(d.getWidth(), d.getHeight()); + } + return size; + } + + public static Size getOptimalPreviewSize(Activity currentActivity, + List sizes, double targetRatio) { + // Use a very small tolerance because we want an exact match. + final double ASPECT_TOLERANCE = 0.001; + if (sizes == null) return null; + + Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + + // Because of bugs of overlay and layout, we sometimes will try to + // layout the viewfinder in the portrait orientation and thus get the + // wrong size of preview surface. When we change the preview size, the + // new overlay will be created before the old one closed, which causes + // an exception. For now, just get the screen size. + Point point = getDefaultDisplaySize(currentActivity, new Point()); + int targetHeight = Math.min(point.x, point.y); + // Try to find an size match aspect ratio and size + for (Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + // Cannot find the one match the aspect ratio. This should not happen. + // Ignore the requirement. + if (optimalSize == null) { + Log.w(TAG, "No preview size match the aspect ratio"); + minDiff = Double.MAX_VALUE; + for (Size size : sizes) { + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + } + return optimalSize; + } + + // Returns the largest picture size which matches the given aspect ratio. + public static Size getOptimalVideoSnapshotPictureSize( + List sizes, double targetRatio) { + // Use a very small tolerance because we want an exact match. + final double ASPECT_TOLERANCE = 0.001; + if (sizes == null) return null; + + Size optimalSize = null; + + // Try to find a size matches aspect ratio and has the largest width + for (Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; + if (optimalSize == null || size.width > optimalSize.width) { + optimalSize = size; + } + } + + // Cannot find one that matches the aspect ratio. This should not happen. + // Ignore the requirement. + if (optimalSize == null) { + Log.w(TAG, "No picture size match the aspect ratio"); + for (Size size : sizes) { + if (optimalSize == null || size.width > optimalSize.width) { + optimalSize = size; + } + } + } + return optimalSize; + } + + public static void dumpParameters(Parameters parameters) { + String flattened = parameters.flatten(); + StringTokenizer tokenizer = new StringTokenizer(flattened, ";"); + Log.d(TAG, "Dump all camera parameters:"); + while (tokenizer.hasMoreElements()) { + Log.d(TAG, tokenizer.nextToken()); + } + } + + /** + * Returns whether the device is voice-capable (meaning, it can do MMS). + */ + public static boolean isMmsCapable(Context context) { + TelephonyManager telephonyManager = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + return false; + } + + try { + Class partypes[] = new Class[0]; + Method sIsVoiceCapable = TelephonyManager.class.getMethod( + "isVoiceCapable", partypes); + + Object arglist[] = new Object[0]; + Object retobj = sIsVoiceCapable.invoke(telephonyManager, arglist); + return (Boolean) retobj; + } catch (java.lang.reflect.InvocationTargetException ite) { + // Failure, must be another device. + // Assume that it is voice capable. + } catch (IllegalAccessException iae) { + // Failure, must be an other device. + // Assume that it is voice capable. + } catch (NoSuchMethodException nsme) { + } + return true; + } + + // This is for test only. Allow the camera to launch the specific camera. + public static int getCameraFacingIntentExtras(Activity currentActivity) { + int cameraId = -1; + + int intentCameraId = + currentActivity.getIntent().getIntExtra(Util.EXTRAS_CAMERA_FACING, -1); + + if (isFrontCameraIntent(intentCameraId)) { + // Check if the front camera exist + int frontCameraId = CameraHolder.instance().getFrontCameraId(); + if (frontCameraId != -1) { + cameraId = frontCameraId; + } + } else if (isBackCameraIntent(intentCameraId)) { + // Check if the back camera exist + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId != -1) { + cameraId = backCameraId; + } + } + return cameraId; + } + + private static boolean isFrontCameraIntent(int intentCameraId) { + return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + private static boolean isBackCameraIntent(int intentCameraId) { + return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK); + } + + private static int sLocation[] = new int[2]; + + // This method is not thread-safe. + public static boolean pointInView(float x, float y, View v) { + v.getLocationInWindow(sLocation); + return x >= sLocation[0] && x < (sLocation[0] + v.getWidth()) + && y >= sLocation[1] && y < (sLocation[1] + v.getHeight()); + } + + public static int[] getRelativeLocation(View reference, View view) { + reference.getLocationInWindow(sLocation); + int referenceX = sLocation[0]; + int referenceY = sLocation[1]; + view.getLocationInWindow(sLocation); + sLocation[0] -= referenceX; + sLocation[1] -= referenceY; + return sLocation; + } + + public static boolean isUriValid(Uri uri, ContentResolver resolver) { + if (uri == null) return false; + + try { + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + if (pfd == null) { + Log.e(TAG, "Fail to open URI. URI=" + uri); + return false; + } + pfd.close(); + } catch (IOException ex) { + return false; + } + return true; + } + + public static void viewUri(Uri uri, Context context) { + if (!isUriValid(uri, context.getContentResolver())) { + Log.e(TAG, "Uri invalid. uri=" + uri); + return; + } + + try { + context.startActivity(new Intent(Util.REVIEW_ACTION, uri)); + } catch (ActivityNotFoundException ex) { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "review image fail. uri=" + uri, e); + } + } + } + + public static void dumpRect(RectF rect, String msg) { + Log.v(TAG, msg + "=(" + rect.left + "," + rect.top + + "," + rect.right + "," + rect.bottom + ")"); + } + + public static void rectFToRect(RectF rectF, Rect rect) { + rect.left = Math.round(rectF.left); + rect.top = Math.round(rectF.top); + rect.right = Math.round(rectF.right); + rect.bottom = Math.round(rectF.bottom); + } + + public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation, + int viewWidth, int viewHeight) { + // Need mirror for front camera. + matrix.setScale(mirror ? -1 : 1, 1); + // This is the value for android.hardware.Camera.setDisplayOrientation. + matrix.postRotate(displayOrientation); + // Camera driver coordinates range from (-1000, -1000) to (1000, 1000). + // UI coordinates range from (0, 0) to (width, height). + matrix.postScale(viewWidth / 2000f, viewHeight / 2000f); + matrix.postTranslate(viewWidth / 2f, viewHeight / 2f); + } + + public static String createJpegName(long dateTaken) { + synchronized (sImageFileNamer) { + return sImageFileNamer.generateName(dateTaken); + } + } + + public static void broadcastNewPicture(Context context, Uri uri) { + context.sendBroadcast(new Intent(ACTION_NEW_PICTURE, uri)); + // Keep compatibility + context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri)); + } + + public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) { + if (view.getVisibility() == View.VISIBLE) return; + + view.setVisibility(View.VISIBLE); + Animation animation = new AlphaAnimation(startAlpha, endAlpha); + animation.setDuration(duration); + view.startAnimation(animation); + } + + public static void fadeIn(View view) { + fadeIn(view, 0F, 1F, 400); + + // We disabled the button in fadeOut(), so enable it here. + view.setEnabled(true); + } + + public static void fadeOut(View view) { + if (view.getVisibility() != View.VISIBLE) return; + + // Since the button is still clickable before fade-out animation + // ends, we disable the button first to block click. + view.setEnabled(false); + Animation animation = new AlphaAnimation(1F, 0F); + animation.setDuration(400); + view.startAnimation(animation); + view.setVisibility(View.GONE); + } + + public static int getJpegRotation(int cameraId, int orientation) { + // See android.hardware.Camera.Parameters.setRotation for + // documentation. + int rotation = 0; + if (orientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId]; + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + rotation = (info.orientation - orientation + 360) % 360; + } else { // back-facing camera + rotation = (info.orientation + orientation) % 360; + } + } + return rotation; + } + + public static void setGpsParameters(Parameters parameters, Location loc) { + // Clear previous GPS location from the parameters. + parameters.removeGpsData(); + + // We always encode GpsTimeStamp + parameters.setGpsTimestamp(System.currentTimeMillis() / 1000); + + // Set GPS location. + if (loc != null) { + double lat = loc.getLatitude(); + double lon = loc.getLongitude(); + boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d); + + if (hasLatLon) { + Log.d(TAG, "Set gps location"); + parameters.setGpsLatitude(lat); + parameters.setGpsLongitude(lon); + parameters.setGpsProcessingMethod(loc.getProvider().toUpperCase()); + if (loc.hasAltitude()) { + parameters.setGpsAltitude(loc.getAltitude()); + } else { + // for NETWORK_PROVIDER location provider, we may have + // no altitude information, but the driver needs it, so + // we fake one. + parameters.setGpsAltitude(0); + } + if (loc.getTime() != 0) { + // Location.getTime() is UTC in milliseconds. + // gps-timestamp is UTC in seconds. + long utcTimeSeconds = loc.getTime() / 1000; + parameters.setGpsTimestamp(utcTimeSeconds); + } + } else { + loc = null; + } + } + } + + private static class ImageFileNamer { + private SimpleDateFormat mFormat; + + // The date (in milliseconds) used to generate the last name. + private long mLastDate; + + // Number of names generated for the same second. + private int mSameSecondCount; + + public ImageFileNamer(String format) { + mFormat = new SimpleDateFormat(format); + } + + public String generateName(long dateTaken) { + Date date = new Date(dateTaken); + String result = mFormat.format(date); + + // If the last name was generated for the same second, + // we append _1, _2, etc to the name. + if (dateTaken / 1000 == mLastDate / 1000) { + mSameSecondCount++; + result += "_" + mSameSecondCount; + } else { + mLastDate = dateTaken; + mSameSecondCount = 0; + } + + return result; + } + } +} diff --git a/src/com/android/camera/VideoController.java b/src/com/android/camera/VideoController.java new file mode 100644 index 000000000..d84c1ad1f --- /dev/null +++ b/src/com/android/camera/VideoController.java @@ -0,0 +1,186 @@ +/* + * 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; + +import android.content.Context; +import android.view.LayoutInflater; + +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.ListPrefSettingPopup; +import com.android.camera.ui.MoreSettingPopup; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.TimeIntervalPopup; + +public class VideoController extends PieController + implements MoreSettingPopup.Listener, + ListPrefSettingPopup.Listener, + TimeIntervalPopup.Listener { + + + private static String TAG = "CAM_videocontrol"; + private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2; + + private VideoModule mModule; + private String[] mOtherKeys; + private AbstractSettingPopup mPopup; + + private static final int POPUP_NONE = 0; + private static final int POPUP_FIRST_LEVEL = 1; + private static final int POPUP_SECOND_LEVEL = 2; + private int mPopupStatus; + + public VideoController(CameraActivity activity, VideoModule module, PieRenderer pie) { + super(activity, pie); + mModule = module; + } + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + mPopupStatus = POPUP_NONE; + float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2; + + addItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep); + addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep); + PieItem item = makeItem(R.drawable.ic_switch_video_facing_holo_light); + item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep); + item.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(PieItem item) { + // Find the index of next camera. + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + if (pref != null) { + int index = pref.findIndexOfValue(pref.getValue()); + CharSequence[] values = pref.getEntryValues(); + index = (index + 1) % values.length; + int newCameraId = Integer.parseInt((String) values[index]); + mListener.onCameraPickerClicked(newCameraId); + } + } + }); + mRenderer.addItem(item); + mOtherKeys = new String[] { + CameraSettings.KEY_VIDEO_EFFECT, + CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, + CameraSettings.KEY_VIDEO_QUALITY, + CameraSettings.KEY_RECORD_LOCATION}; + + item = makeItem(R.drawable.ic_settings_holo_light); + item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) { + initializePopup(); + mPopupStatus = POPUP_FIRST_LEVEL; + } + mModule.showPopup(mPopup); + } + }); + mRenderer.addItem(item); + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + @Override + public void reloadPreferences() { + super.reloadPreferences(); + if (mPopup != null) { + mPopup.reloadPreference(); + } + } + + @Override + public void overrideSettings(final String ... keyvalues) { + super.overrideSettings(keyvalues); + if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) { + mPopupStatus = POPUP_FIRST_LEVEL; + initializePopup(); + } + ((MoreSettingPopup) mPopup).overrideSettings(keyvalues); + } + + @Override + // Hit when an item in the second-level popup gets selected + public void onListPrefChanged(ListPreference pref) { + if (mPopup != null) { + if (mPopupStatus == POPUP_SECOND_LEVEL) { + mModule.dismissPopup(true); + } + } + super.onSettingChanged(pref); + } + + protected void initializePopup() { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate( + R.layout.more_setting_popup, null, false); + popup.setSettingChangedListener(this); + popup.initialize(mPreferenceGroup, mOtherKeys); + if (mActivity.isSecureCamera()) { + // Prevent location preference from getting changed in secure camera mode + popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false); + } + mPopup = popup; + } + + public void popupDismissed(boolean topPopupOnly) { + // if the 2nd level popup gets dismissed + if (mPopupStatus == POPUP_SECOND_LEVEL) { + initializePopup(); + mPopupStatus = POPUP_FIRST_LEVEL; + if (topPopupOnly) mModule.showPopup(mPopup); + } + } + + @Override + // Hit when an item in the first-level popup gets selected, then bring up + // the second-level popup + public void onPreferenceClicked(ListPreference pref) { + if (mPopupStatus != POPUP_FIRST_LEVEL) return; + + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + if (CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL.equals(pref.getKey())) { + TimeIntervalPopup timeInterval = (TimeIntervalPopup) inflater.inflate( + R.layout.time_interval_popup, null, false); + timeInterval.initialize((IconListPreference) pref); + timeInterval.setSettingChangedListener(this); + mModule.dismissPopup(true); + mPopup = timeInterval; + } else { + ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate( + R.layout.list_pref_setting_popup, null, false); + basic.initialize(pref); + basic.setSettingChangedListener(this); + mModule.dismissPopup(true); + mPopup = basic; + } + mModule.showPopup(mPopup); + mPopupStatus = POPUP_SECOND_LEVEL; + } + +} diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java new file mode 100644 index 000000000..d32234a95 --- /dev/null +++ b/src/com/android/camera/VideoModule.java @@ -0,0 +1,2816 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences.Editor; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.Size; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.CameraProfile; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.provider.MediaStore.Video; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.PreviewSurfaceView; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.Rotatable; +import com.android.camera.ui.RotateImageView; +import com.android.camera.ui.RotateLayout; +import com.android.camera.ui.RotateTextToast; +import com.android.camera.ui.TwoStateImageView; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.AccessibilityUtils; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +public class VideoModule implements CameraModule, + CameraPreference.OnPreferenceChangedListener, + ShutterButton.OnShutterButtonListener, + MediaRecorder.OnErrorListener, + MediaRecorder.OnInfoListener, + EffectsRecorder.EffectsListener, + PieRenderer.PieListener { + + private static final String TAG = "CAM_VideoModule"; + + // We number the request code from 1000 to avoid collision with Gallery. + private static final int REQUEST_EFFECT_BACKDROPPER = 1000; + + private static final int CHECK_DISPLAY_ROTATION = 3; + private static final int CLEAR_SCREEN_DELAY = 4; + private static final int UPDATE_RECORD_TIME = 5; + private static final int ENABLE_SHUTTER_BUTTON = 6; + private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7; + private static final int SWITCH_CAMERA = 8; + private static final int SWITCH_CAMERA_START_ANIMATION = 9; + private static final int HIDE_SURFACE_VIEW = 10; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms + + /** + * An unpublished intent flag requesting to start recording straight away + * and return as soon as recording is stopped. + * TODO: consider publishing by moving into MediaStore. + */ + private static final String EXTRA_QUICK_CAPTURE = + "android.intent.extra.quickCapture"; + + private static final int MIN_THUMB_SIZE = 64; + // module fields + private CameraActivity mActivity; + private View mRootView; + private boolean mPaused; + private int mCameraId; + private Parameters mParameters; + + private boolean mSnapshotInProgress = false; + + private static final String EFFECT_BG_FROM_GALLERY = "gallery"; + + private final CameraErrorCallback mErrorCallback = new CameraErrorCallback(); + + private ComboPreferences mPreferences; + private PreferenceGroup mPreferenceGroup; + + private PreviewFrameLayout mPreviewFrameLayout; + private boolean mSurfaceViewReady; + private SurfaceHolder.Callback mSurfaceViewCallback; + private PreviewSurfaceView mPreviewSurfaceView; + private CameraScreenNail.OnFrameDrawnListener mFrameDrawnListener; + private View mReviewControl; + + // An review image having same size as preview. It is displayed when + // recording is stopped in capture intent. + private ImageView mReviewImage; + private Rotatable mReviewCancelButton; + private Rotatable mReviewDoneButton; + private RotateImageView mReviewPlayButton; + private ShutterButton mShutterButton; + private TextView mRecordingTimeView; + private RotateLayout mBgLearningMessageRotater; + private View mBgLearningMessageFrame; + private LinearLayout mLabelsLinearLayout; + + private boolean mIsVideoCaptureIntent; + private boolean mQuickCapture; + + private MediaRecorder mMediaRecorder; + private EffectsRecorder mEffectsRecorder; + private boolean mEffectsDisplayResult; + + private int mEffectType = EffectsRecorder.EFFECT_NONE; + private Object mEffectParameter = null; + private String mEffectUriFromGallery = null; + private String mPrefVideoEffectDefault; + private boolean mResetEffect = true; + + private boolean mSwitchingCamera; + private boolean mMediaRecorderRecording = false; + private long mRecordingStartTime; + private boolean mRecordingTimeCountsDown = false; + private RotateLayout mRecordingTimeRect; + private long mOnResumeTime; + // The video file that the hardware camera is about to record into + // (or is recording into.) + private String mVideoFilename; + private ParcelFileDescriptor mVideoFileDescriptor; + + // The video file that has already been recorded, and that is being + // examined by the user. + private String mCurrentVideoFilename; + private Uri mCurrentVideoUri; + private ContentValues mCurrentVideoValues; + + private CamcorderProfile mProfile; + + // The video duration limit. 0 menas no limit. + private int mMaxVideoDurationInMs; + + // Time Lapse parameters. + private boolean mCaptureTimeLapse = false; + // Default 0. If it is larger than 0, the camcorder is in time lapse mode. + private int mTimeBetweenTimeLapseFrameCaptureMs = 0; + private View mTimeLapseLabel; + + private int mDesiredPreviewWidth; + private int mDesiredPreviewHeight; + + boolean mPreviewing = false; // True if preview is started. + // The display rotation in degrees. This is only valid when mPreviewing is + // true. + private int mDisplayRotation; + private int mCameraDisplayOrientation; + + private ContentResolver mContentResolver; + + private LocationManager mLocationManager; + + private VideoNamer mVideoNamer; + + private RenderOverlay mRenderOverlay; + private PieRenderer mPieRenderer; + + private VideoController mVideoControl; + private AbstractSettingPopup mPopup; + private int mPendingSwitchCameraId; + + private ZoomRenderer mZoomRenderer; + + private PreviewGestures mGestures; + private View mMenu; + private View mBlocker; + private View mOnScreenIndicators; + private ImageView mFlashIndicator; + + private final Handler mHandler = new MainHandler(); + + // The degrees of the device rotated clockwise from its natural orientation. + private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + + private int mZoomValue; // The current zoom value. + private int mZoomMax; + private List mZoomRatios; + private boolean mRestoreFlash; // This is used to check if we need to restore the flash + // status when going back from gallery. + + protected class CameraOpenThread extends Thread { + @Override + public void run() { + openCamera(); + } + } + + private void openCamera() { + try { + mActivity.mCameraDevice = Util.openCamera(mActivity, mCameraId); + mParameters = mActivity.mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + mActivity.mOpenCameraFail = true; + } catch (CameraDisabledException e) { + mActivity.mCameraDisabled = true; + } + } + + // This Handler is used to post message back onto the main thread of the + // application + private class MainHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case ENABLE_SHUTTER_BUTTON: + mShutterButton.setEnabled(true); + break; + + case CLEAR_SCREEN_DELAY: { + mActivity.getWindow().clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + break; + } + + case UPDATE_RECORD_TIME: { + updateRecordingTime(); + break; + } + + case CHECK_DISPLAY_ROTATION: { + // Restart the preview if display rotation has changed. + // Sometimes this happens when the device is held upside + // down and camera app is opened. Rotation animation will + // take some time and the rotation value we have got may be + // wrong. Framework does not have a callback for this now. + if ((Util.getDisplayRotation(mActivity) != mDisplayRotation) + && !mMediaRecorderRecording && !mSwitchingCamera) { + startPreview(); + } + if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) { + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + break; + } + + case SHOW_TAP_TO_SNAPSHOT_TOAST: { + showTapToSnapshotToast(); + break; + } + + case SWITCH_CAMERA: { + switchCamera(); + break; + } + + case SWITCH_CAMERA_START_ANIMATION: { + ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + + // Enable all camera controls. + mSwitchingCamera = false; + break; + } + + case HIDE_SURFACE_VIEW: { + mPreviewSurfaceView.setVisibility(View.GONE); + break; + } + + default: + Log.v(TAG, "Unhandled message: " + msg.what); + break; + } + } + } + + private BroadcastReceiver mReceiver = null; + + private class MyBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_MEDIA_EJECT)) { + stopVideoRecording(); + } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { + Toast.makeText(mActivity, + mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show(); + } + } + } + + private String createName(long dateTaken) { + Date date = new Date(dateTaken); + SimpleDateFormat dateFormat = new SimpleDateFormat( + mActivity.getString(R.string.video_file_name_format)); + + return dateFormat.format(date); + } + + private int getPreferredCameraId(ComboPreferences preferences) { + int intentCameraId = Util.getCameraFacingIntentExtras(mActivity); + if (intentCameraId != -1) { + // Testing purpose. Launch a specific camera through the intent + // extras. + return intentCameraId; + } else { + return CameraSettings.readPreferredCameraId(preferences); + } + } + + private void initializeSurfaceView() { + mPreviewSurfaceView = (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view); + if (!ApiHelper.HAS_SURFACE_TEXTURE) { // API level < 11 + if (mSurfaceViewCallback == null) { + mSurfaceViewCallback = new SurfaceViewCallback(); + } + mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback); + mPreviewSurfaceView.setVisibility(View.VISIBLE); + } else if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { // API level < 16 + if (mSurfaceViewCallback == null) { + mSurfaceViewCallback = new SurfaceViewCallback(); + mFrameDrawnListener = new CameraScreenNail.OnFrameDrawnListener() { + @Override + public void onFrameDrawn(CameraScreenNail c) { + mHandler.sendEmptyMessage(HIDE_SURFACE_VIEW); + } + }; + } + mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback); + } + } + + private void initializeOverlay() { + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mVideoControl = new VideoController(mActivity, this, mPieRenderer); + mVideoControl.setListener(this); + mPieRenderer.setPieListener(this); + } + mRenderOverlay.addRenderer(mPieRenderer); + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + } + mRenderOverlay.addRenderer(mZoomRenderer); + if (mGestures == null) { + mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer); + } + mGestures.setRenderOverlay(mRenderOverlay); + mGestures.clearTouchReceivers(); + mGestures.addTouchReceiver(mMenu); + mGestures.addTouchReceiver(mBlocker); + + if (isVideoCaptureIntent()) { + if (mReviewCancelButton != null) { + mGestures.addTouchReceiver((View) mReviewCancelButton); + } + if (mReviewDoneButton != null) { + mGestures.addTouchReceiver((View) mReviewDoneButton); + } + if (mReviewPlayButton != null) { + mGestures.addTouchReceiver((View) mReviewPlayButton); + } + } + } + + @Override + public void init(CameraActivity activity, View root, boolean reuseScreenNail) { + mActivity = activity; + mRootView = root; + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mCameraId = getPreferredCameraId(mPreferences); + + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + + mActivity.mNumberOfCameras = CameraHolder.instance().getNumberOfCameras(); + mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default); + resetEffect(); + + /* + * To reduce startup time, we start the preview in another thread. + * We make sure the preview is started at the end of onCreate. + */ + CameraOpenThread cameraOpenThread = new CameraOpenThread(); + cameraOpenThread.start(); + + mContentResolver = mActivity.getContentResolver(); + + mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsVideoCaptureIntent = isVideoCaptureIntent(); + if (reuseScreenNail) { + mActivity.reuseCameraScreenNail(!mIsVideoCaptureIntent); + } else { + mActivity.createCameraScreenNail(!mIsVideoCaptureIntent); + } + initializeSurfaceView(); + + // Make sure camera device is opened. + try { + cameraOpenThread.join(); + if (mActivity.mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } else if (mActivity.mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + } catch (InterruptedException ex) { + // ignore + } + + readVideoPreferences(); + new Thread(new Runnable() { + @Override + public void run() { + startPreview(); + } + }).start(); + + initializeControlByIntent(); + initializeOverlay(); + initializeMiscControls(); + + mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); + mLocationManager = new LocationManager(mActivity, null); + + setOrientationIndicator(0, false); + setDisplayOrientation(); + + showTimeLapseUI(mCaptureTimeLapse); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + + initializeVideoControl(); + mPendingSwitchCameraId = -1; + updateOnScreenIndicators(); + } + + @Override + public void onStop() {} + + private void loadCameraPreferences() { + CameraSettings settings = new CameraSettings(mActivity, mParameters, + mCameraId, CameraHolder.instance().getCameraInfo()); + // Remove the video quality preference setting when the quality is given in the intent. + mPreferenceGroup = filterPreferenceScreenByIntent( + settings.getPreferenceGroup(R.xml.video_preferences)); + } + + @Override + public boolean collapseCameraControls() { + boolean ret = false; + if (mPopup != null) { + dismissPopup(false); + ret = true; + } + return ret; + } + + public boolean removeTopLevelPopup() { + if (mPopup != null) { + dismissPopup(true); + return true; + } + return false; + } + + private void enableCameraControls(boolean enable) { + if (mGestures != null) { + mGestures.setZoomOnly(!enable); + } + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + + private void initializeVideoControl() { + loadCameraPreferences(); + mVideoControl.initialize(mPreferenceGroup); + if (effectsActive()) { + mVideoControl.overrideSettings( + CameraSettings.KEY_VIDEO_QUALITY, + Integer.toString(getLowVideoQuality())); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static int getLowVideoQuality() { + if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) { + return CamcorderProfile.QUALITY_480P; + } else { + return CamcorderProfile.QUALITY_LOW; + } + } + + + @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 == OrientationEventListener.ORIENTATION_UNKNOWN) return; + int newOrientation = Util.roundOrientation(orientation, mOrientation); + + if (mOrientation != newOrientation) { + mOrientation = newOrientation; + // The input of effects recorder is affected by + // android.hardware.Camera.setDisplayOrientation. Its value only + // compensates the camera orientation (no Display.getRotation). + // So the orientation hint here should only consider sensor + // orientation. + if (effectsActive()) { + mEffectsRecorder.setOrientationHint(mOrientation); + } + } + + // Show the toast after getting the first orientation changed. + if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) { + mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST); + showTapToSnapshotToast(); + } + } + + private void setOrientationIndicator(int orientation, boolean animation) { + Rotatable[] indicators = { + mBgLearningMessageRotater, + mReviewDoneButton, mReviewPlayButton}; + for (Rotatable indicator : indicators) { + if (indicator != null) indicator.setOrientation(orientation, animation); + } + if (mGestures != null) { + mGestures.setOrientation(orientation); + } + + // We change the orientation of the review cancel button only for tablet + // UI because there's a label along with the X icon. For phone UI, we + // don't change the orientation because there's only a symmetrical X + // icon. + if (mReviewCancelButton instanceof RotateLayout) { + mReviewCancelButton.setOrientation(orientation, animation); + } + + // We change the orientation of the linearlayout only for phone UI because when in portrait + // the width is not enough. + if (mLabelsLinearLayout != null) { + if (((orientation / 90) & 1) == 0) { + mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL); + } else { + mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL); + } + } + mRecordingTimeRect.setOrientation(0, animation); + } + + private void startPlayVideoActivity() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat)); + try { + mActivity.startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); + } + } + + @OnClickAttr + public void onReviewPlayClicked(View v) { + startPlayVideoActivity(); + } + + @OnClickAttr + public void onReviewDoneClicked(View v) { + doReturnToCaller(true); + } + + @OnClickAttr + public void onReviewCancelClicked(View v) { + stopVideoRecording(); + doReturnToCaller(false); + } + + private void onStopVideoRecording() { + mEffectsDisplayResult = true; + boolean recordFail = stopVideoRecording(); + if (mIsVideoCaptureIntent) { + if (!effectsActive()) { + if (mQuickCapture) { + doReturnToCaller(!recordFail); + } else if (!recordFail) { + showAlert(); + } + } + } else if (!recordFail){ + // Start capture animation. + if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // The capture animation is disabled on ICS because we use SurfaceView + // for preview during recording. When the recording is done, we switch + // back to use SurfaceTexture for preview and we need to stop then start + // the preview. This will cause the preview flicker since the preview + // will not be continuous for a short period of time. + ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + } + } + } + + public void onProtectiveCurtainClick(View v) { + // Consume clicks + } + + @Override + public void onShutterButtonClick() { + if (collapseCameraControls() || mSwitchingCamera) return; + + boolean stop = mMediaRecorderRecording; + + if (stop) { + onStopVideoRecording(); + } else { + startVideoRecording(); + } + mShutterButton.setEnabled(false); + + // Keep the shutter button disabled when in video capture intent + // mode and recording is stopped. It'll be re-enabled when + // re-take button is clicked. + if (!(mIsVideoCaptureIntent && stop)) { + mHandler.sendEmptyMessageDelayed( + ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + // Do nothing (everything happens in onShutterButtonClick). + } + + private void readVideoPreferences() { + // The preference stores values from ListPreference and is thus string type for all values. + // We need to convert it to int manually. + String defaultQuality = CameraSettings.getDefaultVideoQuality(mCameraId, + mActivity.getResources().getString(R.string.pref_video_quality_default)); + String videoQuality = + mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY, + defaultQuality); + int quality = Integer.valueOf(videoQuality); + + // Set video quality. + Intent intent = mActivity.getIntent(); + if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { + int extraVideoQuality = + intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); + if (extraVideoQuality > 0) { + quality = CamcorderProfile.QUALITY_HIGH; + } else { // 0 is mms. + quality = CamcorderProfile.QUALITY_LOW; + } + } + + // Set video duration limit. The limit is read from the preference, + // unless it is specified in the intent. + if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { + int seconds = + intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0); + mMaxVideoDurationInMs = 1000 * seconds; + } else { + mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity); + } + + // Set effect + mEffectType = CameraSettings.readEffectType(mPreferences); + if (mEffectType != EffectsRecorder.EFFECT_NONE) { + mEffectParameter = CameraSettings.readEffectParameter(mPreferences); + // Set quality to be no higher than 480p. + CamcorderProfile profile = CamcorderProfile.get(mCameraId, quality); + if (profile.videoFrameHeight > 480) { + quality = getLowVideoQuality(); + } + } else { + mEffectParameter = null; + } + // Read time lapse recording interval. + if (ApiHelper.HAS_TIME_LAPSE_RECORDING) { + String frameIntervalStr = mPreferences.getString( + CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, + mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default)); + mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr); + mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0); + } + // TODO: This should be checked instead directly +1000. + if (mCaptureTimeLapse) quality += 1000; + mProfile = CamcorderProfile.get(mCameraId, quality); + getDesiredPreviewSize(); + } + + private void writeDefaultEffectToPrefs() { + ComboPreferences.Editor editor = mPreferences.edit(); + editor.putString(CameraSettings.KEY_VIDEO_EFFECT, + mActivity.getString(R.string.pref_video_effect_default)); + editor.apply(); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private void getDesiredPreviewSize() { + mParameters = mActivity.mCameraDevice.getParameters(); + if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) { + if (mParameters.getSupportedVideoSizes() == null || effectsActive()) { + mDesiredPreviewWidth = mProfile.videoFrameWidth; + mDesiredPreviewHeight = mProfile.videoFrameHeight; + } else { // Driver supports separates outputs for preview and video. + List sizes = mParameters.getSupportedPreviewSizes(); + Size preferred = mParameters.getPreferredPreviewSizeForVideo(); + int product = preferred.width * preferred.height; + Iterator it = sizes.iterator(); + // Remove the preview sizes that are not preferred. + while (it.hasNext()) { + Size size = it.next(); + if (size.width * size.height > product) { + it.remove(); + } + } + Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes, + (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); + mDesiredPreviewWidth = optimalSize.width; + mDesiredPreviewHeight = optimalSize.height; + } + } else { + mDesiredPreviewWidth = mProfile.videoFrameWidth; + mDesiredPreviewHeight = mProfile.videoFrameHeight; + } + Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth + + ". mDesiredPreviewHeight=" + mDesiredPreviewHeight); + } + + private void resizeForPreviewAspectRatio() { + mPreviewFrameLayout.setAspectRatio( + (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); + } + + @Override + public void installIntentFilter() { + // install an intent filter to receive SD card related events. + IntentFilter intentFilter = + new IntentFilter(Intent.ACTION_MEDIA_EJECT); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); + intentFilter.addDataScheme("file"); + mReceiver = new MyBroadcastReceiver(); + mActivity.registerReceiver(mReceiver, intentFilter); + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + if (mActivity.mOpenCameraFail || mActivity.mCameraDisabled) + return; + + mZoomValue = 0; + + showVideoSnapshotUI(false); + + + if (!mPreviewing) { + if (resetEffect()) { + mBgLearningMessageFrame.setVisibility(View.GONE); + } + openCamera(); + if (mActivity.mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, + R.string.cannot_connect_camera); + return; + } else if (mActivity.mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + readVideoPreferences(); + resizeForPreviewAspectRatio(); + new Thread(new Runnable() { + @Override + public void run() { + startPreview(); + } + }).start(); + } + + // Initializing it here after the preview is started. + initializeZoom(); + + keepScreenOnAwhile(); + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get(mPreferences, + mContentResolver); + mLocationManager.recordLocation(recordLocation); + + if (mPreviewing) { + mOnResumeTime = SystemClock.uptimeMillis(); + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + + mVideoNamer = new VideoNamer(); + } + + private void setDisplayOrientation() { + mDisplayRotation = Util.getDisplayRotation(mActivity); + if (ApiHelper.HAS_SURFACE_TEXTURE) { + // The display rotation is handled by gallery. + mCameraDisplayOrientation = Util.getDisplayOrientation(0, mCameraId); + } else { + // We need to consider display rotation ourselves. + mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId); + } + // GLRoot also uses the DisplayRotation, and needs to be told to layout to update + mActivity.getGLRoot().requestLayoutContentPane(); + } + + private void startPreview() { + Log.v(TAG, "startPreview"); + + mActivity.mCameraDevice.setErrorCallback(mErrorCallback); + if (mPreviewing == true) { + stopPreview(); + if (effectsActive() && mEffectsRecorder != null) { + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + } + + mPreviewing = true; + + setDisplayOrientation(); + mActivity.mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + setCameraParameters(); + + try { + if (!effectsActive()) { + if (ApiHelper.HAS_SURFACE_TEXTURE) { + SurfaceTexture surfaceTexture = ((CameraScreenNail) mActivity.mCameraScreenNail) + .getSurfaceTexture(); + if (surfaceTexture == null) { + return; // The texture has been destroyed (pause, etc) + } + mActivity.mCameraDevice.setPreviewTextureAsync(surfaceTexture); + } else { + mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder()); + } + mActivity.mCameraDevice.startPreviewAsync(); + } else { + initializeEffectsPreview(); + mEffectsRecorder.startPreview(); + } + } catch (Throwable ex) { + closeCamera(); + throw new RuntimeException("startPreview failed", ex); + } finally { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mActivity.mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + } else if (mActivity.mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + } + } + }); + } + } + + private void stopPreview() { + mActivity.mCameraDevice.stopPreview(); + mPreviewing = false; + } + + // Closing the effects out. Will shut down the effects graph. + private void closeEffects() { + Log.v(TAG, "Closing effects"); + mEffectType = EffectsRecorder.EFFECT_NONE; + if (mEffectsRecorder == null) { + Log.d(TAG, "Effects are already closed. Nothing to do"); + return; + } + // This call can handle the case where the camera is already released + // after the recording has been stopped. + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + + // By default, we want to close the effects as well with the camera. + private void closeCamera() { + closeCamera(true); + } + + // In certain cases, when the effects are active, we may want to shutdown + // only the camera related parts, and handle closing the effects in the + // effectsUpdate callback. + // For example, in onPause, we want to make the camera available to + // outside world immediately, however, want to wait till the effects + // callback to shut down the effects. In such a case, we just disconnect + // the effects from the camera by calling disconnectCamera. That way + // the effects can handle that when shutting down. + // + // @param closeEffectsAlso - indicates whether we want to close the + // effects also along with the camera. + private void closeCamera(boolean closeEffectsAlso) { + Log.v(TAG, "closeCamera"); + if (mActivity.mCameraDevice == null) { + Log.d(TAG, "already stopped."); + return; + } + + if (mEffectsRecorder != null) { + // Disconnect the camera from effects so that camera is ready to + // be released to the outside world. + mEffectsRecorder.disconnectCamera(); + } + if (closeEffectsAlso) closeEffects(); + mActivity.mCameraDevice.setZoomChangeListener(null); + mActivity.mCameraDevice.setErrorCallback(null); + CameraHolder.instance().release(); + mActivity.mCameraDevice = null; + mPreviewing = false; + mSnapshotInProgress = false; + } + + private void releasePreviewResources() { + if (ApiHelper.HAS_SURFACE_TEXTURE) { + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + screenNail.releaseSurfaceTexture(); + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + mHandler.removeMessages(HIDE_SURFACE_VIEW); + mPreviewSurfaceView.setVisibility(View.GONE); + } + } + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + + if (mMediaRecorderRecording) { + // Camera will be released in onStopVideoRecording. + onStopVideoRecording(); + } else { + closeCamera(); + if (!effectsActive()) releaseMediaRecorder(); + } + if (effectsActive()) { + // If the effects are active, make sure we tell the graph that the + // surfacetexture is not valid anymore. Disconnect the graph from + // the display. This should be done before releasing the surface + // texture. + mEffectsRecorder.disconnectDisplay(); + } else { + // Close the file descriptor and clear the video namer only if the + // effects are not active. If effects are active, we need to wait + // till we get the callback from the Effects that the graph is done + // recording. That also needs a change in the stopVideoRecording() + // call to not call closeCamera if the effects are active, because + // that will close down the effects are well, thus making this if + // condition invalid. + closeVideoFileDescriptor(); + clearVideoNamer(); + } + + releasePreviewResources(); + + if (mReceiver != null) { + mActivity.unregisterReceiver(mReceiver); + mReceiver = null; + } + resetScreenOn(); + + if (mLocationManager != null) mLocationManager.recordLocation(false); + + mHandler.removeMessages(CHECK_DISPLAY_ROTATION); + mHandler.removeMessages(SWITCH_CAMERA); + mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION); + mPendingSwitchCameraId = -1; + mSwitchingCamera = false; + // Call onPause after stopping video recording. So the camera can be + // released as soon as possible. + } + + @Override + public void onPauseAfterSuper() { + } + + @Override + public void onUserInteraction() { + if (!mMediaRecorderRecording && !mActivity.isFinishing()) { + keepScreenOnAwhile(); + } + } + + @Override + public boolean onBackPressed() { + if (mPaused) return true; + if (mMediaRecorderRecording) { + onStopVideoRecording(); + return true; + } else if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + return true; + } else { + return removeTopLevelPopup(); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Do not handle any key if the activity is paused. + if (mPaused) { + return true; + } + + switch (keyCode) { + case KeyEvent.KEYCODE_CAMERA: + if (event.getRepeatCount() == 0) { + mShutterButton.performClick(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + if (event.getRepeatCount() == 0) { + mShutterButton.performClick(); + return true; + } + break; + case KeyEvent.KEYCODE_MENU: + if (mMediaRecorderRecording) return true; + break; + } + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CAMERA: + mShutterButton.setPressed(false); + return true; + } + return false; + } + + private boolean isVideoCaptureIntent() { + String action = mActivity.getIntent().getAction(); + return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action)); + } + + private void doReturnToCaller(boolean valid) { + Intent resultIntent = new Intent(); + int resultCode; + if (valid) { + resultCode = Activity.RESULT_OK; + resultIntent.setData(mCurrentVideoUri); + } else { + resultCode = Activity.RESULT_CANCELED; + } + mActivity.setResultEx(resultCode, resultIntent); + mActivity.finish(); + } + + private void cleanupEmptyFile() { + if (mVideoFilename != null) { + File f = new File(mVideoFilename); + if (f.length() == 0 && f.delete()) { + Log.v(TAG, "Empty video file deleted: " + mVideoFilename); + mVideoFilename = null; + } + } + } + + private void setupMediaRecorderPreviewDisplay() { + // Nothing to do here if using SurfaceTexture. + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface()); + } else if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // We stop the preview here before unlocking the device because we + // need to change the SurfaceTexture to SurfaceView for preview. + stopPreview(); + mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder()); + // The orientation for SurfaceTexture is different from that for + // SurfaceView. For SurfaceTexture we don't need to consider the + // display rotation. Just consider the sensor's orientation and we + // will set the orientation correctly when showing the texture. + // Gallery will handle the orientation for the preview. For + // SurfaceView we will have to take everything into account so the + // display rotation is considered. + mActivity.mCameraDevice.setDisplayOrientation( + Util.getDisplayOrientation(mDisplayRotation, mCameraId)); + mActivity.mCameraDevice.startPreviewAsync(); + mPreviewing = true; + mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface()); + } + } + + // Prepares media recorder. + private void initializeRecorder() { + Log.v(TAG, "initializeRecorder"); + // If the mCameraDevice is null, then this activity is going to finish + if (mActivity.mCameraDevice == null) return; + + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING && ApiHelper.HAS_SURFACE_TEXTURE) { + // Set the SurfaceView to visible so the surface gets created. + // surfaceCreated() is called immediately when the visibility is + // changed to visible. Thus, mSurfaceViewReady should become true + // right after calling setVisibility(). + mPreviewSurfaceView.setVisibility(View.VISIBLE); + if (!mSurfaceViewReady) return; + } + + Intent intent = mActivity.getIntent(); + Bundle myExtras = intent.getExtras(); + + long requestedSizeLimit = 0; + closeVideoFileDescriptor(); + if (mIsVideoCaptureIntent && myExtras != null) { + Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (saveUri != null) { + try { + mVideoFileDescriptor = + mContentResolver.openFileDescriptor(saveUri, "rw"); + mCurrentVideoUri = saveUri; + } catch (java.io.FileNotFoundException ex) { + // invalid uri + Log.e(TAG, ex.toString()); + } + } + requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); + } + mMediaRecorder = new MediaRecorder(); + + setupMediaRecorderPreviewDisplay(); + // Unlock the camera object before passing it to media recorder. + mActivity.mCameraDevice.unlock(); + mMediaRecorder.setCamera(mActivity.mCameraDevice.getCamera()); + if (!mCaptureTimeLapse) { + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + } + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + mMediaRecorder.setProfile(mProfile); + mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs); + if (mCaptureTimeLapse) { + double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs; + setCaptureRate(mMediaRecorder, fps); + } + + setRecordLocation(); + + // Set output file. + // Try Uri in the intent first. If it doesn't exist, use our own + // instead. + if (mVideoFileDescriptor != null) { + mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); + } else { + generateVideoFilename(mProfile.fileFormat); + mMediaRecorder.setOutputFile(mVideoFilename); + } + + // Set maximum file size. + long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD; + if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { + maxFileSize = requestedSizeLimit; + } + + try { + mMediaRecorder.setMaxFileSize(maxFileSize); + } catch (RuntimeException exception) { + // We are going to ignore failure of setMaxFileSize here, as + // a) The composer selected may simply not support it, or + // b) The underlying media framework may not handle 64-bit range + // on the size restriction. + } + + // See android.hardware.Camera.Parameters.setRotation for + // documentation. + // Note that mOrientation here is the device orientation, which is the opposite of + // what activity.getWindowManager().getDefaultDisplay().getRotation() would return, + // which is the orientation the graphics need to rotate in order to render correctly. + int rotation = 0; + if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + rotation = (info.orientation - mOrientation + 360) % 360; + } else { // back-facing camera + rotation = (info.orientation + mOrientation) % 360; + } + } + mMediaRecorder.setOrientationHint(rotation); + + try { + mMediaRecorder.prepare(); + } catch (IOException e) { + Log.e(TAG, "prepare failed for " + mVideoFilename, e); + releaseMediaRecorder(); + throw new RuntimeException(e); + } + + mMediaRecorder.setOnErrorListener(this); + mMediaRecorder.setOnInfoListener(this); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static void setCaptureRate(MediaRecorder recorder, double fps) { + recorder.setCaptureRate(fps); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setRecordLocation() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + Location loc = mLocationManager.getCurrentLocation(); + if (loc != null) { + mMediaRecorder.setLocation((float) loc.getLatitude(), + (float) loc.getLongitude()); + } + } + } + + private void initializeEffectsPreview() { + Log.v(TAG, "initializeEffectsPreview"); + // If the mCameraDevice is null, then this activity is going to finish + if (mActivity.mCameraDevice == null) return; + + boolean inLandscape = (mActivity.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE); + + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + + mEffectsDisplayResult = false; + mEffectsRecorder = new EffectsRecorder(mActivity); + + // TODO: Confirm none of the following need to go to initializeEffectsRecording() + // and none of these change even when the preview is not refreshed. + mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation); + mEffectsRecorder.setCamera(mActivity.mCameraDevice); + mEffectsRecorder.setCameraFacing(info.facing); + mEffectsRecorder.setProfile(mProfile); + mEffectsRecorder.setEffectsListener(this); + mEffectsRecorder.setOnInfoListener(this); + mEffectsRecorder.setOnErrorListener(this); + + // The input of effects recorder is affected by + // android.hardware.Camera.setDisplayOrientation. Its value only + // compensates the camera orientation (no Display.getRotation). So the + // orientation hint here should only consider sensor orientation. + int orientation = 0; + if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + orientation = mOrientation; + } + mEffectsRecorder.setOrientationHint(orientation); + + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + mEffectsRecorder.setPreviewSurfaceTexture(screenNail.getSurfaceTexture(), + screenNail.getWidth(), screenNail.getHeight()); + + if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER && + ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) { + mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery); + } else { + mEffectsRecorder.setEffect(mEffectType, mEffectParameter); + } + } + + private void initializeEffectsRecording() { + Log.v(TAG, "initializeEffectsRecording"); + + Intent intent = mActivity.getIntent(); + Bundle myExtras = intent.getExtras(); + + long requestedSizeLimit = 0; + closeVideoFileDescriptor(); + if (mIsVideoCaptureIntent && myExtras != null) { + Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (saveUri != null) { + try { + mVideoFileDescriptor = + mContentResolver.openFileDescriptor(saveUri, "rw"); + mCurrentVideoUri = saveUri; + } catch (java.io.FileNotFoundException ex) { + // invalid uri + Log.e(TAG, ex.toString()); + } + } + requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); + } + + mEffectsRecorder.setProfile(mProfile); + // important to set the capture rate to zero if not timelapsed, since the + // effectsrecorder object does not get created again for each recording + // session + if (mCaptureTimeLapse) { + mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs)); + } else { + mEffectsRecorder.setCaptureRate(0); + } + + // Set output file + if (mVideoFileDescriptor != null) { + mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); + } else { + generateVideoFilename(mProfile.fileFormat); + mEffectsRecorder.setOutputFile(mVideoFilename); + } + + // Set maximum file size. + long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD; + if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { + maxFileSize = requestedSizeLimit; + } + mEffectsRecorder.setMaxFileSize(maxFileSize); + mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs); + } + + + private void releaseMediaRecorder() { + Log.v(TAG, "Releasing media recorder."); + if (mMediaRecorder != null) { + cleanupEmptyFile(); + mMediaRecorder.reset(); + mMediaRecorder.release(); + mMediaRecorder = null; + } + mVideoFilename = null; + } + + private void releaseEffectsRecorder() { + Log.v(TAG, "Releasing effects recorder."); + if (mEffectsRecorder != null) { + cleanupEmptyFile(); + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + mEffectType = EffectsRecorder.EFFECT_NONE; + mVideoFilename = null; + } + + private void generateVideoFilename(int outputFileFormat) { + long dateTaken = System.currentTimeMillis(); + String title = createName(dateTaken); + // Used when emailing. + String filename = title + convertOutputFormatToFileExt(outputFileFormat); + String mime = convertOutputFormatToMimeType(outputFileFormat); + String path = Storage.DIRECTORY + '/' + filename; + String tmpPath = path + ".tmp"; + mCurrentVideoValues = new ContentValues(7); + mCurrentVideoValues.put(Video.Media.TITLE, title); + mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename); + mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken); + mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime); + mCurrentVideoValues.put(Video.Media.DATA, path); + mCurrentVideoValues.put(Video.Media.RESOLUTION, + Integer.toString(mProfile.videoFrameWidth) + "x" + + Integer.toString(mProfile.videoFrameHeight)); + Location loc = mLocationManager.getCurrentLocation(); + if (loc != null) { + mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude()); + mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude()); + } + mVideoNamer.prepareUri(mContentResolver, mCurrentVideoValues); + mVideoFilename = tmpPath; + Log.v(TAG, "New video filename: " + mVideoFilename); + } + + private boolean addVideoToMediaStore() { + boolean fail = false; + if (mVideoFileDescriptor == null) { + mCurrentVideoValues.put(Video.Media.SIZE, + new File(mCurrentVideoFilename).length()); + long duration = SystemClock.uptimeMillis() - mRecordingStartTime; + if (duration > 0) { + if (mCaptureTimeLapse) { + duration = getTimeLapseVideoLength(duration); + } + mCurrentVideoValues.put(Video.Media.DURATION, duration); + } else { + Log.w(TAG, "Video duration <= 0 : " + duration); + } + try { + mCurrentVideoUri = mVideoNamer.getUri(); + mActivity.addSecureAlbumItemIfNeeded(true, mCurrentVideoUri); + + // Rename the video file to the final name. This avoids other + // apps reading incomplete data. We need to do it after the + // above mVideoNamer.getUri() call, so we are certain that the + // previous insert to MediaProvider is completed. + String finalName = mCurrentVideoValues.getAsString( + Video.Media.DATA); + if (new File(mCurrentVideoFilename).renameTo(new File(finalName))) { + mCurrentVideoFilename = finalName; + } + + mContentResolver.update(mCurrentVideoUri, mCurrentVideoValues + , null, null); + mActivity.sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO, + mCurrentVideoUri)); + } catch (Exception e) { + // We failed to insert into the database. This can happen if + // the SD card is unmounted. + Log.e(TAG, "failed to add video to media store", e); + mCurrentVideoUri = null; + mCurrentVideoFilename = null; + fail = true; + } finally { + Log.v(TAG, "Current video URI: " + mCurrentVideoUri); + } + } + mCurrentVideoValues = null; + return fail; + } + + private void deleteCurrentVideo() { + // Remove the video and the uri if the uri is not passed in by intent. + if (mCurrentVideoFilename != null) { + deleteVideoFile(mCurrentVideoFilename); + mCurrentVideoFilename = null; + if (mCurrentVideoUri != null) { + mContentResolver.delete(mCurrentVideoUri, null, null); + mCurrentVideoUri = null; + } + } + mActivity.updateStorageSpaceAndHint(); + } + + private void deleteVideoFile(String fileName) { + Log.v(TAG, "Deleting video " + fileName); + File f = new File(fileName); + if (!f.delete()) { + Log.v(TAG, "Could not delete " + fileName); + } + } + + private PreferenceGroup filterPreferenceScreenByIntent( + PreferenceGroup screen) { + Intent intent = mActivity.getIntent(); + if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { + CameraSettings.removePreferenceFromScreen(screen, + CameraSettings.KEY_VIDEO_QUALITY); + } + + if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { + CameraSettings.removePreferenceFromScreen(screen, + CameraSettings.KEY_VIDEO_QUALITY); + } + return screen; + } + + // from MediaRecorder.OnErrorListener + @Override + public void onError(MediaRecorder mr, int what, int extra) { + Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra); + if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { + // We may have run out of space on the sdcard. + stopVideoRecording(); + mActivity.updateStorageSpaceAndHint(); + } + } + + // from MediaRecorder.OnInfoListener + @Override + public void onInfo(MediaRecorder mr, int what, int extra) { + if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { + if (mMediaRecorderRecording) onStopVideoRecording(); + } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { + if (mMediaRecorderRecording) onStopVideoRecording(); + + // Show the toast. + Toast.makeText(mActivity, R.string.video_reach_size_limit, + Toast.LENGTH_LONG).show(); + } + } + + /* + * Make sure we're not recording music playing in the background, ask the + * MediaPlaybackService to pause playback. + */ + private void pauseAudioPlayback() { + // Shamelessly copied from MediaPlaybackService.java, which + // should be public, but isn't. + Intent i = new Intent("com.android.music.musicservicecommand"); + i.putExtra("command", "pause"); + + mActivity.sendBroadcast(i); + } + + // For testing. + public boolean isRecording() { + return mMediaRecorderRecording; + } + + private void startVideoRecording() { + Log.v(TAG, "startVideoRecording"); + mActivity.setSwipingEnabled(false); + + mActivity.updateStorageSpaceAndHint(); + if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) { + Log.v(TAG, "Storage issue, ignore the start request"); + return; + } + + mCurrentVideoUri = null; + if (effectsActive()) { + initializeEffectsRecording(); + if (mEffectsRecorder == null) { + Log.e(TAG, "Fail to initialize effect recorder"); + return; + } + } else { + initializeRecorder(); + if (mMediaRecorder == null) { + Log.e(TAG, "Fail to initialize media recorder"); + return; + } + } + + pauseAudioPlayback(); + + if (effectsActive()) { + try { + mEffectsRecorder.startRecording(); + } catch (RuntimeException e) { + Log.e(TAG, "Could not start effects recorder. ", e); + releaseEffectsRecorder(); + return; + } + } else { + try { + mMediaRecorder.start(); // Recording is now started + } catch (RuntimeException e) { + Log.e(TAG, "Could not start media recorder. ", e); + releaseMediaRecorder(); + // If start fails, frameworks will not lock the camera for us. + mActivity.mCameraDevice.lock(); + return; + } + } + + // Make sure the video recording has started before announcing + // this in accessibility. + AccessibilityUtils.makeAnnouncement(mShutterButton, + mActivity.getString(R.string.video_recording_started)); + + // The parameters may have been changed by MediaRecorder upon starting + // recording. We need to alter the parameters if we support camcorder + // zoom. To reduce latency when setting the parameters during zoom, we + // update mParameters here once. + if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) { + mParameters = mActivity.mCameraDevice.getParameters(); + } + + enableCameraControls(false); + + mMediaRecorderRecording = true; + mActivity.getOrientationManager().lockOrientation(); + mRecordingStartTime = SystemClock.uptimeMillis(); + showRecordingUI(true); + + updateRecordingTime(); + keepScreenOn(); + } + + private void showRecordingUI(boolean recording) { + mMenu.setVisibility(recording ? View.GONE : View.VISIBLE); + mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE); + if (recording) { + mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording); + mActivity.hideSwitcher(); + mRecordingTimeView.setText(""); + mRecordingTimeView.setVisibility(View.VISIBLE); + if (mReviewControl != null) mReviewControl.setVisibility(View.GONE); + // The camera is not allowed to be accessed in older api levels during + // recording. It is therefore necessary to hide the zoom UI on older + // platforms. + // See the documentation of android.media.MediaRecorder.start() for + // further explanation. + if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING + && mParameters.isZoomSupported()) { + // TODO: disable zoom UI here. + } + } else { + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + mActivity.showSwitcher(); + mRecordingTimeView.setVisibility(View.GONE); + if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE); + if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING + && mParameters.isZoomSupported()) { + // TODO: enable zoom UI here. + } + } + } + + private void showAlert() { + Bitmap bitmap = null; + if (mVideoFileDescriptor != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(), + mPreviewFrameLayout.getWidth()); + } else if (mCurrentVideoFilename != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename, + mPreviewFrameLayout.getWidth()); + } + if (bitmap != null) { + // MetadataRetriever already rotates the thumbnail. We should rotate + // it to match the UI orientation (and mirror if it is front-facing camera). + CameraInfo[] info = CameraHolder.instance().getCameraInfo(); + boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT); + bitmap = Util.rotateAndMirror(bitmap, 0, mirror); + mReviewImage.setImageBitmap(bitmap); + mReviewImage.setVisibility(View.VISIBLE); + } + + Util.fadeOut(mShutterButton); + + Util.fadeIn((View) mReviewDoneButton); + Util.fadeIn(mReviewPlayButton); + mMenu.setVisibility(View.GONE); + mOnScreenIndicators.setVisibility(View.GONE); + enableCameraControls(false); + + showTimeLapseUI(false); + } + + private void hideAlert() { + mReviewImage.setVisibility(View.GONE); + mShutterButton.setEnabled(true); + mMenu.setVisibility(View.VISIBLE); + mOnScreenIndicators.setVisibility(View.VISIBLE); + enableCameraControls(true); + + Util.fadeOut((View) mReviewDoneButton); + Util.fadeOut(mReviewPlayButton); + + Util.fadeIn(mShutterButton); + + if (mCaptureTimeLapse) { + showTimeLapseUI(true); + } + } + + private boolean stopVideoRecording() { + Log.v(TAG, "stopVideoRecording"); + mActivity.setSwipingEnabled(true); + mActivity.showSwitcher(); + + boolean fail = false; + if (mMediaRecorderRecording) { + boolean shouldAddToMediaStoreNow = false; + + try { + if (effectsActive()) { + // This is asynchronous, so we can't add to media store now because thumbnail + // may not be ready. In such case addVideoToMediaStore is called later + // through a callback from the MediaEncoderFilter to EffectsRecorder, + // and then to the VideoModule. + mEffectsRecorder.stopRecording(); + } else { + mMediaRecorder.setOnErrorListener(null); + mMediaRecorder.setOnInfoListener(null); + mMediaRecorder.stop(); + shouldAddToMediaStoreNow = true; + } + mCurrentVideoFilename = mVideoFilename; + Log.v(TAG, "stopVideoRecording: Setting current video filename: " + + mCurrentVideoFilename); + AccessibilityUtils.makeAnnouncement(mShutterButton, + mActivity.getString(R.string.video_recording_stopped)); + } catch (RuntimeException e) { + Log.e(TAG, "stop fail", e); + if (mVideoFilename != null) deleteVideoFile(mVideoFilename); + fail = true; + } + mMediaRecorderRecording = false; + mActivity.getOrientationManager().unlockOrientation(); + + // If the activity is paused, this means activity is interrupted + // during recording. Release the camera as soon as possible because + // face unlock or other applications may need to use the camera. + // However, if the effects are active, then we can only release the + // camera and cannot release the effects recorder since that will + // stop the graph. It is possible to separate out the Camera release + // part and the effects release part. However, the effects recorder + // does hold on to the camera, hence, it needs to be "disconnected" + // from the camera in the closeCamera call. + if (mPaused) { + // Closing only the camera part if effects active. Effects will + // be closed in the callback from effects. + boolean closeEffects = !effectsActive(); + closeCamera(closeEffects); + } + + showRecordingUI(false); + if (!mIsVideoCaptureIntent) { + enableCameraControls(true); + } + // The orientation was fixed during video recording. Now make it + // reflect the device orientation as video recording is stopped. + setOrientationIndicator(0, true); + keepScreenOnAwhile(); + if (shouldAddToMediaStoreNow) { + if (addVideoToMediaStore()) fail = true; + } + } + // always release media recorder if no effects running + if (!effectsActive()) { + releaseMediaRecorder(); + if (!mPaused) { + mActivity.mCameraDevice.lock(); + if (ApiHelper.HAS_SURFACE_TEXTURE && + !ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + stopPreview(); + // Switch back to use SurfaceTexture for preview. + ((CameraScreenNail) mActivity.mCameraScreenNail).setOneTimeOnFrameDrawnListener( + mFrameDrawnListener); + startPreview(); + } + } + } + // Update the parameters here because the parameters might have been altered + // by MediaRecorder. + if (!mPaused) mParameters = mActivity.mCameraDevice.getParameters(); + return fail; + } + + private void resetScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + private void keepScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) { + long seconds = milliSeconds / 1000; // round down to compute seconds + long minutes = seconds / 60; + long hours = minutes / 60; + long remainderMinutes = minutes - (hours * 60); + long remainderSeconds = seconds - (minutes * 60); + + StringBuilder timeStringBuilder = new StringBuilder(); + + // Hours + if (hours > 0) { + if (hours < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(hours); + + timeStringBuilder.append(':'); + } + + // Minutes + if (remainderMinutes < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderMinutes); + timeStringBuilder.append(':'); + + // Seconds + if (remainderSeconds < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderSeconds); + + // Centi seconds + if (displayCentiSeconds) { + timeStringBuilder.append('.'); + long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10; + if (remainderCentiSeconds < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderCentiSeconds); + } + + return timeStringBuilder.toString(); + } + + private long getTimeLapseVideoLength(long deltaMs) { + // For better approximation calculate fractional number of frames captured. + // This will update the video time at a higher resolution. + double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs; + return (long) (numberOfFrames / mProfile.videoFrameRate * 1000); + } + + private void updateRecordingTime() { + if (!mMediaRecorderRecording) { + return; + } + long now = SystemClock.uptimeMillis(); + long delta = now - mRecordingStartTime; + + // Starting a minute before reaching the max duration + // limit, we'll countdown the remaining time instead. + boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0 + && delta >= mMaxVideoDurationInMs - 60000); + + long deltaAdjusted = delta; + if (countdownRemainingTime) { + deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999; + } + String text; + + long targetNextUpdateDelay; + if (!mCaptureTimeLapse) { + text = millisecondToTimeString(deltaAdjusted, false); + targetNextUpdateDelay = 1000; + } else { + // The length of time lapse video is different from the length + // of the actual wall clock time elapsed. Display the video length + // only in format hh:mm:ss.dd, where dd are the centi seconds. + text = millisecondToTimeString(getTimeLapseVideoLength(delta), true); + targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs; + } + + mRecordingTimeView.setText(text); + + if (mRecordingTimeCountsDown != countdownRemainingTime) { + // Avoid setting the color on every update, do it only + // when it needs changing. + mRecordingTimeCountsDown = countdownRemainingTime; + + int color = mActivity.getResources().getColor(countdownRemainingTime + ? R.color.recording_time_remaining_text + : R.color.recording_time_elapsed_text); + + mRecordingTimeView.setTextColor(color); + } + + long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay); + mHandler.sendEmptyMessageDelayed( + UPDATE_RECORD_TIME, actualNextUpdateDelay); + } + + private static boolean isSupported(String value, List supported) { + return supported == null ? false : supported.indexOf(value) >= 0; + } + + @SuppressWarnings("deprecation") + private void setCameraParameters() { + mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); + mParameters.setPreviewFrameRate(mProfile.videoFrameRate); + + // Set flash mode. + String flashMode; + if (mActivity.mShowCameraAppView) { + flashMode = mPreferences.getString( + CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, + mActivity.getString(R.string.pref_camera_video_flashmode_default)); + } else { + flashMode = Parameters.FLASH_MODE_OFF; + } + List supportedFlash = mParameters.getSupportedFlashModes(); + if (isSupported(flashMode, supportedFlash)) { + mParameters.setFlashMode(flashMode); + } else { + flashMode = mParameters.getFlashMode(); + if (flashMode == null) { + flashMode = mActivity.getString( + R.string.pref_camera_flashmode_no_flash); + } + } + + // Set white balance parameter. + String whiteBalance = mPreferences.getString( + CameraSettings.KEY_WHITE_BALANCE, + mActivity.getString(R.string.pref_camera_whitebalance_default)); + if (isSupported(whiteBalance, + mParameters.getSupportedWhiteBalance())) { + mParameters.setWhiteBalance(whiteBalance); + } else { + whiteBalance = mParameters.getWhiteBalance(); + if (whiteBalance == null) { + whiteBalance = Parameters.WHITE_BALANCE_AUTO; + } + } + + // Set zoom. + if (mParameters.isZoomSupported()) { + mParameters.setZoom(mZoomValue); + } + + // Set continuous autofocus. + List supportedFocus = mParameters.getSupportedFocusModes(); + if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) { + mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + + mParameters.set(Util.RECORDING_HINT, Util.TRUE); + + // Enable video stabilization. Convenience methods not available in API + // level <= 14 + String vstabSupported = mParameters.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + mParameters.set("video-stabilization", "true"); + } + + // Set picture size. + // The logic here is different from the logic in still-mode camera. + // There we determine the preview size based on the picture size, but + // here we determine the picture size based on the preview size. + List supported = mParameters.getSupportedPictureSizes(); + Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported, + (double) mDesiredPreviewWidth / mDesiredPreviewHeight); + Size original = mParameters.getPictureSize(); + if (!original.equals(optimalSize)) { + mParameters.setPictureSize(optimalSize.width, optimalSize.height); + } + Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" + + optimalSize.height); + + // Set JPEG quality. + int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, + CameraProfile.QUALITY_HIGH); + mParameters.setJpegQuality(jpegQuality); + + mActivity.mCameraDevice.setParameters(mParameters); + // Keep preview size up to date. + mParameters = mActivity.mCameraDevice.getParameters(); + + updateCameraScreenNailSize(mDesiredPreviewWidth, mDesiredPreviewHeight); + } + + private void updateCameraScreenNailSize(int width, int height) { + if (!ApiHelper.HAS_SURFACE_TEXTURE) return; + + if (mCameraDisplayOrientation % 180 != 0) { + int tmp = width; + width = height; + height = tmp; + } + + CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; + int oldWidth = screenNail.getWidth(); + int oldHeight = screenNail.getHeight(); + + if (oldWidth != width || oldHeight != height) { + screenNail.setSize(width, height); + screenNail.enableAspectRatioClamping(); + mActivity.notifyScreenNailChanged(); + } + + if (screenNail.getSurfaceTexture() == null) { + screenNail.acquireSurfaceTexture(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_EFFECT_BACKDROPPER: + if (resultCode == Activity.RESULT_OK) { + // onActivityResult() runs before onResume(), so this parameter will be + // seen by startPreview from onResume() + mEffectUriFromGallery = data.getData().toString(); + Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery); + mResetEffect = false; + } else { + mEffectUriFromGallery = null; + Log.w(TAG, "No URI from gallery"); + mResetEffect = true; + } + break; + } + } + + @Override + public void onEffectsUpdate(int effectId, int effectMsg) { + Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg); + if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) { + // Effects have shut down. Hide learning message if any, + // and restart regular preview. + mBgLearningMessageFrame.setVisibility(View.GONE); + checkQualityAndStartPreview(); + } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) { + // This follows the codepath from onStopVideoRecording. + if (mEffectsDisplayResult && !addVideoToMediaStore()) { + if (mIsVideoCaptureIntent) { + if (mQuickCapture) { + doReturnToCaller(true); + } else { + showAlert(); + } + } + } + mEffectsDisplayResult = false; + // In onPause, these were not called if the effects were active. We + // had to wait till the effects recording is complete to do this. + if (mPaused) { + closeVideoFileDescriptor(); + clearVideoNamer(); + } + } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) { + // Enable the shutter button once the preview is complete. + mShutterButton.setEnabled(true); + } else if (effectId == EffectsRecorder.EFFECT_BACKDROPPER) { + switch (effectMsg) { + case EffectsRecorder.EFFECT_MSG_STARTED_LEARNING: + mBgLearningMessageFrame.setVisibility(View.VISIBLE); + break; + case EffectsRecorder.EFFECT_MSG_DONE_LEARNING: + case EffectsRecorder.EFFECT_MSG_SWITCHING_EFFECT: + mBgLearningMessageFrame.setVisibility(View.GONE); + break; + } + } + // In onPause, this was not called if the effects were active. We had to + // wait till the effects completed to do this. + if (mPaused) { + Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused"); + closeEffects(); + } + } + + public void onCancelBgTraining(View v) { + // Remove training message + mBgLearningMessageFrame.setVisibility(View.GONE); + // Write default effect out to shared prefs + writeDefaultEffectToPrefs(); + // Tell VideoCamer to re-init based on new shared pref values. + onSharedPreferenceChanged(); + } + + @Override + public synchronized void onEffectsError(Exception exception, String fileName) { + // TODO: Eventually we may want to show the user an error dialog, and then restart the + // camera and encoder gracefully. For now, we just delete the file and bail out. + if (fileName != null && new File(fileName).exists()) { + deleteVideoFile(fileName); + } + try { + if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException") + .isInstance(exception)) { + Log.w(TAG, "Problem recoding video file. Removing incomplete file."); + return; + } + } catch (ClassNotFoundException ex) { + Log.w(TAG, ex); + } + throw new RuntimeException("Error during recording!", exception); + } + + private void initializeControlByIntent() { + mBlocker = mRootView.findViewById(R.id.blocker); + mMenu = mRootView.findViewById(R.id.menu); + mMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mPieRenderer != null) { + mPieRenderer.showInCenter(); + } + } + }); + mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators); + mFlashIndicator = (ImageView) mRootView.findViewById(R.id.menu_flash_indicator); + if (mIsVideoCaptureIntent) { + mActivity.hideSwitcher(); + // Cannot use RotateImageView for "done" and "cancel" button because + // the tablet layout uses RotateLayout, which cannot be cast to + // RotateImageView. + mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done); + mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel); + mReviewPlayButton = (RotateImageView) mRootView.findViewById(R.id.btn_play); + + ((View) mReviewCancelButton).setVisibility(View.VISIBLE); + + ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewDoneClicked(v); + } + }); + ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewCancelClicked(v); + } + }); + + ((View) mReviewPlayButton).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onReviewPlayClicked(v); + } + }); + + + // Not grayed out upon disabled, to make the follow-up fade-out + // effect look smooth. Note that the review done button in tablet + // layout is not a TwoStateImageView. + if (mReviewDoneButton instanceof TwoStateImageView) { + ((TwoStateImageView) mReviewDoneButton).enableFilter(false); + } + } + } + + private void initializeMiscControls() { + mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame); + mPreviewFrameLayout.setOnLayoutChangeListener(mActivity); + mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image); + + mShutterButton = mActivity.getShutterButton(); + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + mShutterButton.setOnShutterButtonListener(this); + mShutterButton.requestFocus(); + + // Disable the shutter button if effects are ON since it might take + // a little more time for the effects preview to be ready. We do not + // want to allow recording before that happens. The shutter button + // will be enabled when we get the message from effectsrecorder that + // the preview is running. This becomes critical when the camera is + // swapped. + if (effectsActive()) { + mShutterButton.setEnabled(false); + } + + mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time); + mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect); + mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label); + // The R.id.labels can only be found in phone layout. + // That is, mLabelsLinearLayout should be null in tablet layout. + mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels); + + mBgLearningMessageRotater = (RotateLayout) mRootView.findViewById(R.id.bg_replace_message); + mBgLearningMessageFrame = mRootView.findViewById(R.id.bg_replace_message_frame); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setDisplayOrientation(); + + // Change layout in response to configuration change + LayoutInflater inflater = mActivity.getLayoutInflater(); + ((ViewGroup) mRootView).removeAllViews(); + inflater.inflate(R.layout.video_module, (ViewGroup) mRootView); + + // from onCreate() + initializeControlByIntent(); + initializeOverlay(); + initializeSurfaceView(); + initializeMiscControls(); + showTimeLapseUI(mCaptureTimeLapse); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + + // from onResume() + showVideoSnapshotUI(false); + initializeZoom(); + onFullScreenChanged(mActivity.isInCameraApp()); + updateOnScreenIndicators(); + } + + @Override + public void onOverriddenPreferencesClicked() { + } + + @Override + // TODO: Delete this after old camera code is removed + public void onRestorePreferencesClicked() { + } + + private boolean effectsActive() { + return (mEffectType != EffectsRecorder.EFFECT_NONE); + } + + @Override + public void onSharedPreferenceChanged() { + // ignore the events after "onPause()" or preview has not started yet + if (mPaused) return; + synchronized (mPreferences) { + // If mCameraDevice is not ready then we can set the parameter in + // startPreview(). + if (mActivity.mCameraDevice == null) return; + + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + // Check if the current effects selection has changed + if (updateEffectSelection()) return; + + readVideoPreferences(); + showTimeLapseUI(mCaptureTimeLapse); + // We need to restart the preview if preview size is changed. + Size size = mParameters.getPreviewSize(); + if (size.width != mDesiredPreviewWidth + || size.height != mDesiredPreviewHeight) { + if (!effectsActive()) { + stopPreview(); + } else { + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + resizeForPreviewAspectRatio(); + startPreview(); // Parameters will be set in startPreview(). + } else { + setCameraParameters(); + } + updateOnScreenIndicators(); + } + } + + private void updateOnScreenIndicators() { + updateFlashOnScreenIndicator(mParameters.getFlashMode()); + } + + private void updateFlashOnScreenIndicator(String value) { + if (mFlashIndicator == null) { + return; + } + if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } else { + if (Parameters.FLASH_MODE_AUTO.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto); + } else if (Parameters.FLASH_MODE_ON.equals(value) || + Parameters.FLASH_MODE_TORCH.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on); + } else { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } + } + } + + private void switchCamera() { + if (mPaused) return; + + Log.d(TAG, "Start to switch camera."); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + mVideoControl.setCameraId(mCameraId); + + closeCamera(); + + // Restart the camera and initialize the UI. From onCreate. + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + openCamera(); + readVideoPreferences(); + startPreview(); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + initializeVideoControl(); + + // From onResume + initializeZoom(); + setOrientationIndicator(0, false); + + if (ApiHelper.HAS_SURFACE_TEXTURE) { + // Start switch camera animation. Post a message because + // onFrameAvailable from the old camera may already exist. + mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); + } + updateOnScreenIndicators(); + } + + // Preview texture has been copied. Now camera can be released and the + // animation can be started. + @Override + public void onPreviewTextureCopied() { + mHandler.sendEmptyMessage(SWITCH_CAMERA); + } + + @Override + public void onCaptureTextureCopied() { + } + + private boolean updateEffectSelection() { + int previousEffectType = mEffectType; + Object previousEffectParameter = mEffectParameter; + mEffectType = CameraSettings.readEffectType(mPreferences); + mEffectParameter = CameraSettings.readEffectParameter(mPreferences); + + if (mEffectType == previousEffectType) { + if (mEffectType == EffectsRecorder.EFFECT_NONE) return false; + if (mEffectParameter.equals(previousEffectParameter)) return false; + } + Log.v(TAG, "New effect selection: " + mPreferences.getString( + CameraSettings.KEY_VIDEO_EFFECT, "none")); + + if (mEffectType == EffectsRecorder.EFFECT_NONE) { + // Stop effects and return to normal preview + mEffectsRecorder.stopPreview(); + mPreviewing = false; + return true; + } + if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER && + ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) { + // Request video from gallery to use for background + Intent i = new Intent(Intent.ACTION_PICK); + i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI, + "video/*"); + i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER); + return true; + } + if (previousEffectType == EffectsRecorder.EFFECT_NONE) { + // Stop regular preview and start effects. + stopPreview(); + checkQualityAndStartPreview(); + } else { + // Switch currently running effect + mEffectsRecorder.setEffect(mEffectType, mEffectParameter); + } + return true; + } + + // Verifies that the current preview view size is correct before starting + // preview. If not, resets the surface texture and resizes the view. + private void checkQualityAndStartPreview() { + readVideoPreferences(); + showTimeLapseUI(mCaptureTimeLapse); + Size size = mParameters.getPreviewSize(); + if (size.width != mDesiredPreviewWidth + || size.height != mDesiredPreviewHeight) { + resizeForPreviewAspectRatio(); + } + // Start up preview again + startPreview(); + } + + private void showTimeLapseUI(boolean enable) { + if (mTimeLapseLabel != null) { + mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mSwitchingCamera) return true; + if (mPopup == null && mGestures != null && mRenderOverlay != null) { + return mGestures.dispatchTouch(m); + } else if (mPopup != null) { + return mActivity.superDispatchTouchEvent(m); + } + return false; + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int value) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return; + mZoomValue = value; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mActivity.mCameraDevice.setParametersAsync(mParameters); + Parameters p = mActivity.mCameraDevice.getParameters(); + mZoomRenderer.setZoomValue(mZoomRatios.get(p.getZoom())); + } + + @Override + public void onZoomStart() { + } + @Override + public void onZoomEnd() { + } + } + + private void initializeZoom() { + if (!mParameters.isZoomSupported()) return; + mZoomMax = mParameters.getMaxZoom(); + mZoomRatios = mParameters.getZoomRatios(); + // Currently we use immediate zoom for fast zooming to get better UX and + // there is no plan to take advantage of the smooth zoom. + mZoomRenderer.setZoomMax(mZoomMax); + mZoomRenderer.setZoom(mParameters.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(mParameters.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + + private void initializeVideoSnapshot() { + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + mActivity.setSingleTapUpListener(mPreviewFrameLayout); + // Show the tap to focus toast if this is the first start. + if (mPreferences.getBoolean( + CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) { + // Delay the toast for one second to wait for orientation. + mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000); + } + } else { + mActivity.setSingleTapUpListener(null); + } + } + + void showVideoSnapshotUI(boolean enabled) { + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + if (ApiHelper.HAS_SURFACE_TEXTURE && enabled) { + ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + } else { + mPreviewFrameLayout.showBorder(enabled); + } + mShutterButton.setEnabled(!enabled); + } + } + + // Preview area is touched. Take a picture. + @Override + public void onSingleTapUp(View view, int x, int y) { + if (mMediaRecorderRecording && effectsActive()) { + new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint, + mOrientation).show(); + return; + } + + if (mPaused || mSnapshotInProgress || effectsActive()) { + return; + } + + if (!mMediaRecorderRecording) { + // check for dismissing popup + if (mPopup != null) { + dismissPopup(true); + } + return; + } + + // Set rotation and gps data. + int rotation = Util.getJpegRotation(mCameraId, mOrientation); + mParameters.setRotation(rotation); + Location loc = mLocationManager.getCurrentLocation(); + Util.setGpsParameters(mParameters, loc); + mActivity.mCameraDevice.setParameters(mParameters); + + Log.v(TAG, "Video snapshot start"); + mActivity.mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc)); + showVideoSnapshotUI(true); + mSnapshotInProgress = true; + } + + @Override + public void updateCameraAppView() { + if (!mPreviewing || mParameters.getFlashMode() == null) return; + + // When going to and back from gallery, we need to turn off/on the flash. + if (!mActivity.mShowCameraAppView) { + if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) { + mRestoreFlash = false; + return; + } + mRestoreFlash = true; + setCameraParameters(); + } else if (mRestoreFlash) { + mRestoreFlash = false; + setCameraParameters(); + } + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenu != null) { + mMenu.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + @Override + public void onFullScreenChanged(boolean full) { + if (mGestures != null) { + mGestures.setEnabled(full); + } + if (mPopup != null) { + dismissPopup(false, full); + } + if (mRenderOverlay != null) { + // this can not happen in capture mode + mRenderOverlay.setVisibility(full ? View.VISIBLE : View.GONE); + } + setShowMenu(full); + if (mBlocker != null) { + // this can not happen in capture mode + mBlocker.setVisibility(full ? View.VISIBLE : View.GONE); + } + if (ApiHelper.HAS_SURFACE_TEXTURE) { + if (mActivity.mCameraScreenNail != null) { + ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full); + } + return; + } + if (full) { + mPreviewSurfaceView.expand(); + } else { + mPreviewSurfaceView.shrink(); + } + } + + private final class JpegPictureCallback implements PictureCallback { + Location mLocation; + + public JpegPictureCallback(Location loc) { + mLocation = loc; + } + + @Override + public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) { + Log.v(TAG, "onPictureTaken"); + mSnapshotInProgress = false; + showVideoSnapshotUI(false); + storeImage(jpegData, mLocation); + } + } + + private void storeImage(final byte[] data, Location loc) { + long dateTaken = System.currentTimeMillis(); + String title = Util.createJpegName(dateTaken); + int orientation = Exif.getOrientation(data); + Size s = mParameters.getPictureSize(); + Uri uri = Storage.addImage(mContentResolver, title, dateTaken, loc, orientation, data, + s.width, s.height); + if (uri != null) { + Util.broadcastNewPicture(mActivity, uri); + } + } + + private boolean resetEffect() { + if (mResetEffect) { + String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT, + mPrefVideoEffectDefault); + if (!mPrefVideoEffectDefault.equals(value)) { + writeDefaultEffectToPrefs(); + return true; + } + } + mResetEffect = true; + return false; + } + + private String convertOutputFormatToMimeType(int outputFileFormat) { + if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { + return "video/mp4"; + } + return "video/3gpp"; + } + + private String convertOutputFormatToFileExt(int outputFileFormat) { + if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { + return ".mp4"; + } + return ".3gp"; + } + + private void closeVideoFileDescriptor() { + if (mVideoFileDescriptor != null) { + try { + mVideoFileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Fail to close fd", e); + } + mVideoFileDescriptor = null; + } + } + + private void showTapToSnapshotToast() { + new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0) + .show(); + // Clear the preference. + Editor editor = mPreferences.edit(); + editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false); + editor.apply(); + } + + private void clearVideoNamer() { + if (mVideoNamer != null) { + mVideoNamer.finish(); + mVideoNamer = null; + } + } + + private static class VideoNamer extends Thread { + private boolean mRequestPending; + private ContentResolver mResolver; + private ContentValues mValues; + private boolean mStop; + private Uri mUri; + + // Runs in main thread + public VideoNamer() { + start(); + } + + // Runs in main thread + public synchronized void prepareUri( + ContentResolver resolver, ContentValues values) { + mRequestPending = true; + mResolver = resolver; + mValues = new ContentValues(values); + notifyAll(); + } + + // Runs in main thread + public synchronized Uri getUri() { + // wait until the request is done. + while (mRequestPending) { + try { + wait(); + } catch (InterruptedException ex) { + // ignore. + } + } + Uri uri = mUri; + mUri = null; + return uri; + } + + // Runs in namer thread + @Override + public synchronized void run() { + while (true) { + if (mStop) break; + if (!mRequestPending) { + try { + wait(); + } catch (InterruptedException ex) { + // ignore. + } + continue; + } + cleanOldUri(); + generateUri(); + mRequestPending = false; + notifyAll(); + } + cleanOldUri(); + } + + // Runs in main thread + public synchronized void finish() { + mStop = true; + notifyAll(); + } + + // Runs in namer thread + private void generateUri() { + Uri videoTable = Uri.parse("content://media/external/video/media"); + mUri = mResolver.insert(videoTable, mValues); + } + + // Runs in namer thread + private void cleanOldUri() { + if (mUri == null) return; + mResolver.delete(mUri, null, null); + mUri = null; + } + } + + private class SurfaceViewCallback implements SurfaceHolder.Callback { + public SurfaceViewCallback() {} + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.v(TAG, "Surface changed. width=" + width + ". height=" + height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.v(TAG, "Surface created"); + mSurfaceViewReady = true; + if (mPaused) return; + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder()); + if (!mPreviewing) { + startPreview(); + } + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v(TAG, "Surface destroyed"); + mSurfaceViewReady = false; + if (mPaused) return; + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + stopVideoRecording(); + stopPreview(); + } + } + } + + @Override + public boolean updateStorageHintOnResume() { + return true; + } + + // required by OnPreferenceChangedListener + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + if (ApiHelper.HAS_SURFACE_TEXTURE) { + Log.d(TAG, "Start to copy texture."); + // We need to keep a preview frame for the animation before + // releasing the camera. This will trigger onPreviewTextureCopied. + ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); + // Disable all camera controls. + mSwitchingCamera = true; + } else { + switchCamera(); + } + } + + @Override + public boolean needsSwitcher() { + return !mIsVideoCaptureIntent; + } + + @Override + public void onPieOpened(int centerX, int centerY) { + mActivity.cancelActivityTouchHandling(); + mActivity.setSwipingEnabled(false); + } + + @Override + public void onPieClosed() { + mActivity.setSwipingEnabled(true); + } + + public void showPopup(AbstractSettingPopup popup) { + mActivity.hideUI(); + mBlocker.setVisibility(View.INVISIBLE); + setShowMenu(false); + mPopup = popup; + mPopup.setVisibility(View.VISIBLE); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER; + ((FrameLayout) mRootView).addView(mPopup, lp); + } + + public void dismissPopup(boolean topLevelOnly) { + dismissPopup(topLevelOnly, true); + } + + public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) { + if (fullScreen) { + mActivity.showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mVideoControl.popupDismissed(topLevelPopupOnly); + } + + @Override + public void onShowSwitcherPopup() { + if (mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } +} diff --git a/src/com/android/camera/drawable/TextDrawable.java b/src/com/android/camera/drawable/TextDrawable.java new file mode 100644 index 000000000..2e86364e7 --- /dev/null +++ b/src/com/android/camera/drawable/TextDrawable.java @@ -0,0 +1,84 @@ +/* + * 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.drawable; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + + +public class TextDrawable extends Drawable { + + private static final int DEFAULT_COLOR = Color.WHITE; + private static final int DEFAULT_TEXTSIZE = 15; + + private Paint mPaint; + private CharSequence mText; + private int mIntrinsicWidth; + private int mIntrinsicHeight; + + public TextDrawable(Resources res, CharSequence text) { + mText = text; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(DEFAULT_COLOR); + mPaint.setTextAlign(Align.CENTER); + float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + DEFAULT_TEXTSIZE, res.getDisplayMetrics()); + mPaint.setTextSize(textSize); + mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5); + mIntrinsicHeight = mPaint.getFontMetricsInt(null); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + canvas.drawText(mText, 0, mText.length(), + bounds.centerX(), bounds.centerY(), mPaint); + } + + @Override + public int getOpacity() { + return mPaint.getAlpha(); + } + + @Override + public int getIntrinsicWidth() { + return mIntrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return mIntrinsicHeight; + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + } + +} diff --git a/src/com/android/camera/ui/AbstractSettingPopup.java b/src/com/android/camera/ui/AbstractSettingPopup.java new file mode 100644 index 000000000..49df77b30 --- /dev/null +++ b/src/com/android/camera/ui/AbstractSettingPopup.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.camera.R; + +// A popup window that shows one or more camera settings. +abstract public class AbstractSettingPopup extends RotateLayout { + protected ViewGroup mSettingList; + protected TextView mTitle; + + public AbstractSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mTitle = (TextView) findViewById(R.id.title); + mSettingList = (ViewGroup) findViewById(R.id.settingList); + } + + abstract public void reloadPreference(); +} diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java new file mode 100644 index 000000000..7b9fb6499 --- /dev/null +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -0,0 +1,293 @@ +/* + * 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.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.LinearLayout; + +import com.android.camera.R; +import com.android.gallery3d.common.ApiHelper; + +public class CameraSwitcher extends RotateImageView + implements OnClickListener, OnTouchListener { + + private static final String TAG = "CAM_Switcher"; + private static final int SWITCHER_POPUP_ANIM_DURATION = 200; + + 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); + } + + 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)) { + 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; + 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) { + 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_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_new_panorama)); + break; + default: + break; + } + content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); + } + } + + public boolean showsPopup() { + return mShowingPopup; + } + + public boolean isInsidePopup(MotionEvent evt) { + if (!showsPopup()) return false; + return evt.getX() >= mPopup.getLeft() + && evt.getX() < mPopup.getRight() + && evt.getY() >= mPopup.getTop() + && evt.getY() < mPopup.getBottom(); + } + + private void hidePopup() { + mShowingPopup = false; + setVisibility(View.VISIBLE); + if (mPopup != null && !animateHidePopup()) { + mPopup.setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(null); + } + + private void showSwitcher() { + mShowingPopup = true; + if (mPopup == null) { + initPopup(); + } + 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 updateInitialTranslations() { + if (getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT) { + mTranslationX = -getWidth() / 2; + mTranslationY = getHeight(); + } else { + mTranslationX = getWidth(); + mTranslationY = getHeight() / 2; + } + } + private void popupAnimationSetup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return; + } + updateInitialTranslations(); + 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.setVisibility(View.INVISIBLE); + } + } + }; + } + 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); + } + } + }; + } + 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/CheckedLinearLayout.java b/src/com/android/camera/ui/CheckedLinearLayout.java new file mode 100644 index 000000000..4e7750499 --- /dev/null +++ b/src/com/android/camera/ui/CheckedLinearLayout.java @@ -0,0 +1,60 @@ +/* + * 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.widget.Checkable; +import android.widget.LinearLayout; + +public class CheckedLinearLayout extends LinearLayout implements Checkable { + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + private boolean mChecked; + + public CheckedLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + } + } + + @Override + public void toggle() { + setChecked(!mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} diff --git a/src/com/android/camera/ui/CountDownView.java b/src/com/android/camera/ui/CountDownView.java new file mode 100644 index 000000000..ade25c33a --- /dev/null +++ b/src/com/android/camera/ui/CountDownView.java @@ -0,0 +1,131 @@ +/* + * 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 java.util.Locale; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.camera.R; + +public class CountDownView extends FrameLayout { + + private static final String TAG = "CAM_CountDownView"; + private static final int SET_TIMER_TEXT = 1; + private TextView mRemainingSecondsView; + private int mRemainingSecs = 0; + private OnCountDownFinishedListener mListener; + private Animation mCountDownAnim; + private SoundPool mSoundPool; + private int mBeepTwice; + private int mBeepOnce; + private boolean mPlaySound; + private final Handler mHandler = new MainHandler(); + + public CountDownView(Context context, AttributeSet attrs) { + super(context, attrs); + mCountDownAnim = AnimationUtils.loadAnimation(context, R.anim.count_down_exit); + // Load the beeps + mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0); + mBeepOnce = mSoundPool.load(context, R.raw.beep_once, 1); + mBeepTwice = mSoundPool.load(context, R.raw.beep_twice, 1); + } + + public boolean isCountingDown() { + return mRemainingSecs > 0; + }; + + public interface OnCountDownFinishedListener { + public void onCountDownFinished(); + } + + private void remainingSecondsChanged(int newVal) { + mRemainingSecs = newVal; + if (newVal == 0) { + // Countdown has finished + setVisibility(View.INVISIBLE); + mListener.onCountDownFinished(); + } else { + Locale locale = getResources().getConfiguration().locale; + String localizedValue = String.format(locale, "%d", newVal); + mRemainingSecondsView.setText(localizedValue); + // Fade-out animation + mCountDownAnim.reset(); + mRemainingSecondsView.clearAnimation(); + mRemainingSecondsView.startAnimation(mCountDownAnim); + + // Play sound effect for the last 3 seconds of the countdown + if (mPlaySound) { + if (newVal == 1) { + mSoundPool.play(mBeepTwice, 1.0f, 1.0f, 0, 0, 1.0f); + } else if (newVal <= 3) { + mSoundPool.play(mBeepOnce, 1.0f, 1.0f, 0, 0, 1.0f); + } + } + // Schedule the next remainingSecondsChanged() call in 1 second + mHandler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mRemainingSecondsView = (TextView) findViewById(R.id.remaining_seconds); + } + + public void setCountDownFinishedListener(OnCountDownFinishedListener listener) { + mListener = listener; + } + + public void startCountDown(int sec, boolean playSound) { + if (sec <= 0) { + Log.w(TAG, "Invalid input for countdown timer: " + sec + " seconds"); + return; + } + setVisibility(View.VISIBLE); + mPlaySound = playSound; + remainingSecondsChanged(sec); + } + + public void cancelCountDown() { + if (mRemainingSecs > 0) { + mRemainingSecs = 0; + mHandler.removeMessages(SET_TIMER_TEXT); + setVisibility(View.INVISIBLE); + } + } + + private class MainHandler extends Handler { + @Override + public void handleMessage(Message message) { + if (message.what == SET_TIMER_TEXT) { + remainingSecondsChanged(mRemainingSecs -1); + } + } + } +} \ No newline at end of file diff --git a/src/com/android/camera/ui/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java new file mode 100644 index 000000000..628d8155a --- /dev/null +++ b/src/com/android/camera/ui/EffectSettingPopup.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; +import com.android.camera.R; + +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.HashMap; + +// A popup window that shows video effect setting. It has two grid view. +// One shows the goofy face effects. The other shows the background replacer +// effects. +public class EffectSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener, View.OnClickListener { + private static final String TAG = "EffectSettingPopup"; + private String mNoEffect; + private IconListPreference mPreference; + private Listener mListener; + private View mClearEffects; + private GridView mSillyFacesGrid; + private GridView mBackgroundGrid; + + // Data for silly face items. (text, image, and preference value) + ArrayList> mSillyFacesItem = + new ArrayList>(); + + // Data for background replacer items. (text, image, and preference value) + ArrayList> mBackgroundItem = + new ArrayList>(); + + + static public interface Listener { + public void onSettingChanged(); + } + + public EffectSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + mNoEffect = context.getString(R.string.pref_video_effect_default); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mClearEffects = findViewById(R.id.clear_effects); + mClearEffects.setOnClickListener(this); + mSillyFacesGrid = (GridView) findViewById(R.id.effect_silly_faces); + mBackgroundGrid = (GridView) findViewById(R.id.effect_background); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + CharSequence[] entryValues = mPreference.getEntryValues(); + int[] iconIds = mPreference.getImageIds(); + if (iconIds == null) { + iconIds = mPreference.getLargeIconIds(); + } + + // Set title. + mTitle.setText(mPreference.getTitle()); + + for(int i = 0; i < entries.length; ++i) { + String value = entryValues[i].toString(); + if (value.equals(mNoEffect)) continue; // no effect, skip it. + HashMap map = new HashMap(); + map.put("value", value); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + if (value.startsWith("goofy_face")) { + mSillyFacesItem.add(map); + } else if (value.startsWith("backdropper")) { + mBackgroundItem.add(map); + } + } + + boolean hasSillyFaces = mSillyFacesItem.size() > 0; + boolean hasBackground = mBackgroundItem.size() > 0; + + // Initialize goofy face if it is supported. + if (hasSillyFaces) { + findViewById(R.id.effect_silly_faces_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_silly_faces_title_separator).setVisibility(View.VISIBLE); + mSillyFacesGrid.setVisibility(View.VISIBLE); + SimpleAdapter sillyFacesItemAdapter = new SimpleAdapter(context, + mSillyFacesItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mSillyFacesGrid.setAdapter(sillyFacesItemAdapter); + mSillyFacesGrid.setOnItemClickListener(this); + } + + if (hasSillyFaces && hasBackground) { + findViewById(R.id.effect_background_separator).setVisibility(View.VISIBLE); + } + + // Initialize background replacer if it is supported. + if (hasBackground) { + findViewById(R.id.effect_background_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_background_title_separator).setVisibility(View.VISIBLE); + mBackgroundGrid.setVisibility(View.VISIBLE); + SimpleAdapter backgroundItemAdapter = new SimpleAdapter(context, + mBackgroundItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mBackgroundGrid.setAdapter(backgroundItemAdapter); + mBackgroundGrid.setOnItemClickListener(this); + } + + reloadPreference(); + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Do not show or hide "Clear effects" button when the popup + // is already visible. Otherwise it looks strange. + boolean noEffect = mPreference.getValue().equals(mNoEffect); + mClearEffects.setVisibility(noEffect ? View.GONE : View.VISIBLE); + } + reloadPreference(); + } + super.setVisibility(visibility); + } + + // The value of the preference may have changed. Update the UI. + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void reloadPreference() { + mBackgroundGrid.setItemChecked(mBackgroundGrid.getCheckedItemPosition(), false); + mSillyFacesGrid.setItemChecked(mSillyFacesGrid.getCheckedItemPosition(), false); + + String value = mPreference.getValue(); + if (value.equals(mNoEffect)) return; + + for (int i = 0; i < mSillyFacesItem.size(); i++) { + if (value.equals(mSillyFacesItem.get(i).get("value"))) { + mSillyFacesGrid.setItemChecked(i, true); + return; + } + } + + for (int i = 0; i < mBackgroundItem.size(); i++) { + if (value.equals(mBackgroundItem.get(i).get("value"))) { + mBackgroundGrid.setItemChecked(i, true); + return; + } + } + + Log.e(TAG, "Invalid preference value: " + value); + mPreference.print(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView parent, View view, + int index, long id) { + String value; + if (parent == mSillyFacesGrid) { + value = (String) mSillyFacesItem.get(index).get("value"); + } else if (parent == mBackgroundGrid) { + value = (String) mBackgroundItem.get(index).get("value"); + } else { + return; + } + + // Tapping the selected effect will deselect it (clear effects). + if (value.equals(mPreference.getValue())) { + mPreference.setValue(mNoEffect); + } else { + mPreference.setValue(value); + } + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } + + @Override + public void onClick(View v) { + // Clear the effect. + mPreference.setValue(mNoEffect); + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } +} diff --git a/src/com/android/camera/ui/ExpandedGridView.java b/src/com/android/camera/ui/ExpandedGridView.java new file mode 100644 index 000000000..13cf58f34 --- /dev/null +++ b/src/com/android/camera/ui/ExpandedGridView.java @@ -0,0 +1,36 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +public class ExpandedGridView extends GridView { + public ExpandedGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // If UNSPECIFIED is passed to GridView, it will show only one row. + // Here GridView is put in a ScrollView, so pass it a very big size with + // AT_MOST to show all the rows. + heightMeasureSpec = MeasureSpec.makeMeasureSpec(65536, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java new file mode 100644 index 000000000..9e6f98245 --- /dev/null +++ b/src/com/android/camera/ui/FaceView.java @@ -0,0 +1,217 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.hardware.Camera.Face; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.camera.CameraActivity; +import com.android.camera.CameraScreenNail; +import com.android.camera.R; +import com.android.camera.Util; +import com.android.gallery3d.common.ApiHelper; + +@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) +public class FaceView extends View implements FocusIndicator, Rotatable { + private static final String TAG = "CAM FaceView"; + private final boolean LOGV = false; + // The value for android.hardware.Camera.setDisplayOrientation. + private int mDisplayOrientation; + // The orientation compensation for the face indicator to make it look + // correctly in all device orientations. Ex: if the value is 90, the + // indicator should be rotated 90 degrees counter-clockwise. + private int mOrientation; + private boolean mMirror; + private boolean mPause; + private Matrix mMatrix = new Matrix(); + private RectF mRect = new RectF(); + // As face detection can be flaky, we add a layer of filtering on top of it + // to avoid rapid changes in state (eg, flickering between has faces and + // not having faces) + private Face[] mFaces; + private Face[] mPendingFaces; + private int mColor; + private final int mFocusingColor; + private final int mFocusedColor; + private final int mFailColor; + private Paint mPaint; + private volatile boolean mBlocked; + + private static final int MSG_SWITCH_FACES = 1; + private static final int SWITCH_DELAY = 70; + private boolean mStateSwitchPending = false; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SWITCH_FACES: + mStateSwitchPending = false; + mFaces = mPendingFaces; + invalidate(); + break; + } + } + }; + + public FaceView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = getResources(); + mFocusingColor = res.getColor(R.color.face_detect_start); + mFocusedColor = res.getColor(R.color.face_detect_success); + mFailColor = res.getColor(R.color.face_detect_fail); + mColor = mFocusingColor; + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke)); + } + + public void setFaces(Face[] faces) { + if (LOGV) Log.v(TAG, "Num of faces=" + faces.length); + if (mPause) return; + if (mFaces != null) { + if ((faces.length > 0 && mFaces.length == 0) + || (faces.length == 0 && mFaces.length > 0)) { + mPendingFaces = faces; + if (!mStateSwitchPending) { + mStateSwitchPending = true; + mHandler.sendEmptyMessageDelayed(MSG_SWITCH_FACES, SWITCH_DELAY); + } + return; + } + } + if (mStateSwitchPending) { + mStateSwitchPending = false; + mHandler.removeMessages(MSG_SWITCH_FACES); + } + mFaces = faces; + invalidate(); + } + + public void setDisplayOrientation(int orientation) { + mDisplayOrientation = orientation; + if (LOGV) Log.v(TAG, "mDisplayOrientation=" + orientation); + } + + @Override + public void setOrientation(int orientation, boolean animation) { + mOrientation = orientation; + invalidate(); + } + + public void setMirror(boolean mirror) { + mMirror = mirror; + if (LOGV) Log.v(TAG, "mMirror=" + mirror); + } + + public boolean faceExists() { + return (mFaces != null && mFaces.length > 0); + } + + @Override + public void showStart() { + mColor = mFocusingColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showSuccess(boolean timeout) { + mColor = mFocusedColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showFail(boolean timeout) { + mColor = mFailColor; + invalidate(); + } + + @Override + public void clear() { + // Face indicator is displayed during preview. Do not clear the + // drawable. + mColor = mFocusingColor; + mFaces = null; + invalidate(); + } + + public void pause() { + mPause = true; + } + + public void resume() { + mPause = false; + } + + public void setBlockDraw(boolean block) { + mBlocked = block; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!mBlocked && (mFaces != null) && (mFaces.length > 0)) { + final CameraScreenNail sn = ((CameraActivity) getContext()).getCameraScreenNail(); + int rw = sn.getUncroppedRenderWidth(); + int rh = sn.getUncroppedRenderHeight(); + // Prepare the matrix. + if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180))) + || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) { + int temp = rw; + rw = rh; + rh = temp; + } + Util.prepareMatrix(mMatrix, mMirror, mDisplayOrientation, rw, rh); + int dx = (getWidth() - rw) / 2; + int dy = (getHeight() - rh) / 2; + + // Focus indicator is directional. Rotate the matrix and the canvas + // so it looks correctly in all orientations. + canvas.save(); + mMatrix.postRotate(mOrientation); // postRotate is clockwise + canvas.rotate(-mOrientation); // rotate is counter-clockwise (for canvas) + for (int i = 0; i < mFaces.length; i++) { + // Filter out false positives. + if (mFaces[i].score < 50) continue; + + // Transform the coordinates. + mRect.set(mFaces[i].rect); + if (LOGV) Util.dumpRect(mRect, "Original rect"); + mMatrix.mapRect(mRect); + if (LOGV) Util.dumpRect(mRect, "Transformed rect"); + mPaint.setColor(mColor); + mRect.offset(dx, dy); + canvas.drawOval(mRect, mPaint); + } + canvas.restore(); + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/camera/ui/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java new file mode 100644 index 000000000..e06057041 --- /dev/null +++ b/src/com/android/camera/ui/FocusIndicator.java @@ -0,0 +1,24 @@ +/* + * 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.ui; + +public interface FocusIndicator { + public void showStart(); + public void showSuccess(boolean timeout); + public void showFail(boolean timeout); + public void clear(); +} diff --git a/src/com/android/camera/ui/InLineSettingCheckBox.java b/src/com/android/camera/ui/InLineSettingCheckBox.java new file mode 100644 index 000000000..5d9cc388d --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingCheckBox.java @@ -0,0 +1,83 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; + + +import com.android.camera.ListPreference; +import com.android.camera.R; + +/* A check box setting control which turns on/off the setting. */ +public class InLineSettingCheckBox extends InLineSettingItem { + private CheckBox mCheckBox; + + OnCheckedChangeListener mCheckedChangeListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) { + changeIndex(desiredState ? 1 : 0); + } + }; + + public InLineSettingCheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mCheckBox = (CheckBox) findViewById(R.id.setting_check_box); + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + // Add content descriptions for the increment and decrement buttons. + mCheckBox.setContentDescription(getContext().getResources().getString( + R.string.accessibility_check_box, mPreference.getTitle())); + } + + @Override + protected void updateView() { + mCheckBox.setOnCheckedChangeListener(null); + if (mOverrideValue == null) { + mCheckBox.setChecked(mIndex == 1); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + mCheckBox.setChecked(index == 1); + } + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + if (mTitle != null) mTitle.setEnabled(enable); + if (mCheckBox != null) mCheckBox.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/InLineSettingItem.java b/src/com/android/camera/ui/InLineSettingItem.java new file mode 100644 index 000000000..4f88f2738 --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingItem.java @@ -0,0 +1,94 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.camera.R; + +/** + * A one-line camera setting could be one of three types: knob, switch or restore + * preference button. The setting includes a title for showing the preference + * title which is initialized in the SimpleAdapter. A knob also includes + * (ex: Picture size), a previous button, the current value (ex: 5MP), + * and a next button. A switch, i.e. the preference RecordLocationPreference, + * has only two values on and off which will be controlled in a switch button. + * Other setting popup window includes several InLineSettingItem items with + * different types if possible. + */ +public abstract class InLineSettingItem extends LinearLayout { + private Listener mListener; + protected ListPreference mPreference; + protected int mIndex; + // Scene mode can override the original preference value. + protected String mOverrideValue; + protected TextView mTitle; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + } + + public InLineSettingItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void setTitle(ListPreference preference) { + mTitle = ((TextView) findViewById(R.id.title)); + mTitle.setText(preference.getTitle()); + } + + public void initialize(ListPreference preference) { + setTitle(preference); + if (preference == null) return; + mPreference = preference; + reloadPreference(); + } + + protected abstract void updateView(); + + protected boolean changeIndex(int index) { + if (index >= mPreference.getEntryValues().length || index < 0) return false; + mIndex = index; + mPreference.setValueIndex(mIndex); + if (mListener != null) { + mListener.onSettingChanged(mPreference); + } + updateView(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // The value of the preference may have changed. Update the UI. + public void reloadPreference() { + mIndex = mPreference.findIndexOfValue(mPreference.getValue()); + updateView(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public void overrideSettings(String value) { + mOverrideValue = value; + updateView(); + } +} diff --git a/src/com/android/camera/ui/InLineSettingMenu.java b/src/com/android/camera/ui/InLineSettingMenu.java new file mode 100644 index 000000000..2fe89349a --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingMenu.java @@ -0,0 +1,78 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.camera.R; + +/* Setting menu item that will bring up a menu when you click on it. */ +public class InLineSettingMenu extends InLineSettingItem { + private static final String TAG = "InLineSettingMenu"; + // The view that shows the current selected setting. Ex: 5MP + private TextView mEntry; + + public InLineSettingMenu(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mEntry = (TextView) findViewById(R.id.current_setting); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + //TODO: add contentDescription + } + + @Override + protected void updateView() { + if (mOverrideValue == null) { + mEntry.setText(mPreference.getEntry()); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + if (index != -1) { + mEntry.setText(mPreference.getEntries()[index]); + } else { + // Avoid the crash if camera driver has bugs. + Log.e(TAG, "Fail to find override value=" + mOverrideValue); + mPreference.print(); + } + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle() + mPreference.getEntry()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + super.setEnabled(enable); + if (mTitle != null) mTitle.setEnabled(enable); + if (mEntry != null) mEntry.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java new file mode 100644 index 000000000..ef4eb6a7a --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeHelper.java @@ -0,0 +1,43 @@ +/* + * 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 new file mode 100644 index 000000000..6261d34f6 --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeNotifier.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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 new file mode 100644 index 000000000..6e118fc3a --- /dev/null +++ b/src/com/android/camera/ui/LayoutNotifyView.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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/ListPrefSettingPopup.java b/src/com/android/camera/ui/ListPrefSettingPopup.java new file mode 100644 index 000000000..c0411c90d --- /dev/null +++ b/src/com/android/camera/ui/ListPrefSettingPopup.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.camera.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// A popup window that shows one camera setting. The title is the name of the +// setting (ex: white-balance). The entries are the supported values (ex: +// daylight, incandescent, etc). If initialized with an IconListPreference, +// the entries will contain both text and icons. Otherwise, entries will be +// shown in text. +public class ListPrefSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener { + private static final String TAG = "ListPrefSettingPopup"; + private ListPreference mPreference; + private Listener mListener; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public ListPrefSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private class ListPrefSettingAdapter extends SimpleAdapter { + ListPrefSettingAdapter(Context context, List> data, + int resource, String[] from, int[] to) { + super(context, data, resource, from, to); + } + + @Override + public void setViewImage(ImageView v, String value) { + if ("".equals(value)) { + // Some settings have no icons. Ex: exposure compensation. + v.setVisibility(View.GONE); + } else { + super.setViewImage(v, value); + } + } + } + + public void initialize(ListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + int[] iconIds = null; + if (preference instanceof IconListPreference) { + iconIds = ((IconListPreference) mPreference).getImageIds(); + if (iconIds == null) { + iconIds = ((IconListPreference) mPreference).getLargeIconIds(); + } + } + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Prepare the ListView. + ArrayList> listItem = + new ArrayList>(); + for(int i = 0; i < entries.length; ++i) { + HashMap map = new HashMap(); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + listItem.add(map); + } + SimpleAdapter listItemAdapter = new ListPrefSettingAdapter(context, listItem, + R.layout.setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + ((ListView) mSettingList).setAdapter(listItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + reloadPreference(); + } + + // The value of the preference may have changed. Update the UI. + @Override + public void reloadPreference() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index != -1) { + ((ListView) mSettingList).setItemChecked(index, true); + } else { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView parent, View view, + int index, long id) { + mPreference.setValueIndex(index); + if (mListener != null) mListener.onListPrefChanged(mPreference); + } +} diff --git a/src/com/android/camera/ui/MoreSettingPopup.java b/src/com/android/camera/ui/MoreSettingPopup.java new file mode 100644 index 000000000..ab1babaab --- /dev/null +++ b/src/com/android/camera/ui/MoreSettingPopup.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.camera.ListPreference; +import com.android.camera.PreferenceGroup; +import com.android.camera.R; + +import java.util.ArrayList; + +/* A popup window that contains several camera settings. */ +public class MoreSettingPopup extends AbstractSettingPopup + implements InLineSettingItem.Listener, + AdapterView.OnItemClickListener { + @SuppressWarnings("unused") + private static final String TAG = "MoreSettingPopup"; + + private Listener mListener; + private ArrayList mListItem = new ArrayList(); + + // Keep track of which setting items are disabled + // e.g. White balance will be disabled when scene mode is set to non-auto + private boolean[] mEnabled; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + public void onPreferenceClicked(ListPreference pref); + } + + private class MoreSettingAdapter extends ArrayAdapter { + LayoutInflater mInflater; + String mOnString; + String mOffString; + MoreSettingAdapter() { + super(MoreSettingPopup.this.getContext(), 0, mListItem); + Context context = getContext(); + mInflater = LayoutInflater.from(context); + mOnString = context.getString(R.string.setting_on); + mOffString = context.getString(R.string.setting_off); + } + + private int getSettingLayoutId(ListPreference pref) { + + if (isOnOffPreference(pref)) { + return R.layout.in_line_setting_check_box; + } + return R.layout.in_line_setting_menu; + } + + private boolean isOnOffPreference(ListPreference pref) { + CharSequence[] entries = pref.getEntries(); + if (entries.length != 2) return false; + String str1 = entries[0].toString(); + String str2 = entries[1].toString(); + return ((str1.equals(mOnString) && str2.equals(mOffString)) || + (str1.equals(mOffString) && str2.equals(mOnString))); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView != null) return convertView; + + ListPreference pref = mListItem.get(position); + + int viewLayoutId = getSettingLayoutId(pref); + InLineSettingItem view = (InLineSettingItem) + mInflater.inflate(viewLayoutId, parent, false); + + view.initialize(pref); // no init for restore one + view.setSettingChangedListener(MoreSettingPopup.this); + if (position >= 0 && position < mEnabled.length) { + view.setEnabled(mEnabled[position]); + } else { + Log.w(TAG, "Invalid input: enabled list length, " + mEnabled.length + + " position " + position); + } + return view; + } + + @Override + public boolean isEnabled(int position) { + if (position >= 0 && position < mEnabled.length) { + return mEnabled[position]; + } + return true; + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public MoreSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(PreferenceGroup group, String[] keys) { + // Prepare the setting items. + for (int i = 0; i < keys.length; ++i) { + ListPreference pref = group.findPreference(keys[i]); + if (pref != null) mListItem.add(pref); + } + + ArrayAdapter mListItemAdapter = new MoreSettingAdapter(); + ((ListView) mSettingList).setAdapter(mListItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + ((ListView) mSettingList).setSelector(android.R.color.transparent); + // Initialize mEnabled + mEnabled = new boolean[mListItem.size()]; + for (int i = 0; i < mEnabled.length; i++) { + mEnabled[i] = true; + } + } + + // When preferences are disabled, we will display them grayed out. Users + // will not be able to change the disabled preferences, but they can still see + // the current value of the preferences + public void setPreferenceEnabled(String key, boolean enable) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + mEnabled[j] = enable; + break; + } + } + } + + public void onSettingChanged(ListPreference pref) { + if (mListener != null) { + mListener.onSettingChanged(pref); + } + } + + // Scene mode can override other camera settings (ex: flash mode). + public void overrideSettings(final String ... keyvalues) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int i = 0; i < keyvalues.length; i += 2) { + String key = keyvalues[i]; + String value = keyvalues[i + 1]; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + // Change preference + if (value != null) pref.setValue(value); + // If the preference is overridden, disable the preference + boolean enable = value == null; + mEnabled[j] = enable; + if (mSettingList.getChildCount() > j) { + mSettingList.getChildAt(j).setEnabled(enable); + } + } + } + } + reloadPreference(); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + if (mListener != null) { + ListPreference pref = mListItem.get(position); + mListener.onPreferenceClicked(pref); + } + } + + @Override + public void reloadPreference() { + int count = mSettingList.getChildCount(); + for (int i = 0; i < count; i++) { + ListPreference pref = mListItem.get(i); + if (pref != null) { + InLineSettingItem settingItem = + (InLineSettingItem) mSettingList.getChildAt(i); + settingItem.reloadPreference(); + } + } + } +} diff --git a/src/com/android/camera/ui/OnIndicatorEventListener.java b/src/com/android/camera/ui/OnIndicatorEventListener.java new file mode 100644 index 000000000..566f5c7a8 --- /dev/null +++ b/src/com/android/camera/ui/OnIndicatorEventListener.java @@ -0,0 +1,25 @@ +/* + * 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.ui; + +public interface OnIndicatorEventListener { + public static int EVENT_ENTER_SECOND_LEVEL_INDICATOR_BAR = 0; + public static int EVENT_LEAVE_SECOND_LEVEL_INDICATOR_BAR = 1; + public static int EVENT_ENTER_ZOOM_CONTROL = 2; + public static int EVENT_LEAVE_ZOOM_CONTROL = 3; + void onIndicatorEvent(int event); +} diff --git a/src/com/android/camera/ui/OverlayRenderer.java b/src/com/android/camera/ui/OverlayRenderer.java new file mode 100644 index 000000000..417e219aa --- /dev/null +++ b/src/com/android/camera/ui/OverlayRenderer.java @@ -0,0 +1,95 @@ +/* + * 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.graphics.Canvas; +import android.view.MotionEvent; + +public abstract class OverlayRenderer implements RenderOverlay.Renderer { + + private static final String TAG = "CAM OverlayRenderer"; + protected RenderOverlay mOverlay; + + protected int mLeft, mTop, mRight, mBottom; + + protected boolean mVisible; + + public void setVisible(boolean vis) { + mVisible = vis; + update(); + } + + public boolean isVisible() { + return mVisible; + } + + // default does not handle touch + @Override + public boolean handlesTouch() { + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + return false; + } + + public abstract void onDraw(Canvas canvas); + + public void draw(Canvas canvas) { + if (mVisible) { + onDraw(canvas); + } + } + + @Override + public void setOverlay(RenderOverlay overlay) { + mOverlay = overlay; + } + + @Override + public void layout(int left, int top, int right, int bottom) { + mLeft = left; + mRight = right; + mTop = top; + mBottom = bottom; + } + + protected Context getContext() { + if (mOverlay != null) { + return mOverlay.getContext(); + } else { + return null; + } + } + + public int getWidth() { + return mRight - mLeft; + } + + public int getHeight() { + return mBottom - mTop; + } + + protected void update() { + if (mOverlay != null) { + mOverlay.update(); + } + } + +} diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java new file mode 100644 index 000000000..677e5acc8 --- /dev/null +++ b/src/com/android/camera/ui/PieItem.java @@ -0,0 +1,203 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Path; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pie menu item + */ +public class PieItem { + + public static interface OnClickListener { + void onClick(PieItem item); + } + + private Drawable mDrawable; + private int level; + private float mCenter; + private float start; + private float sweep; + private float animate; + private int inner; + private int outer; + private boolean mSelected; + private boolean mEnabled; + private List mItems; + private Path mPath; + private OnClickListener mOnClickListener; + private float mAlpha; + + // Gray out the view when disabled + private static final float ENABLED_ALPHA = 1; + private static final float DISABLED_ALPHA = (float) 0.3; + private boolean mChangeAlphaWhenDisabled = true; + + public PieItem(Drawable drawable, int level) { + mDrawable = drawable; + this.level = level; + setAlpha(1f); + mEnabled = true; + setAnimationAngle(getAnimationAngle()); + start = -1; + mCenter = -1; + } + + public boolean hasItems() { + return mItems != null; + } + + public List getItems() { + return mItems; + } + + public void addItem(PieItem item) { + if (mItems == null) { + mItems = new ArrayList(); + } + mItems.add(item); + } + + public void setPath(Path p) { + mPath = p; + } + + public Path getPath() { + return mPath; + } + + public void setChangeAlphaWhenDisabled (boolean enable) { + mChangeAlphaWhenDisabled = enable; + } + + public void setAlpha(float alpha) { + mAlpha = alpha; + mDrawable.setAlpha((int) (255 * alpha)); + } + + public void setAnimationAngle(float a) { + animate = a; + } + + public float getAnimationAngle() { + return animate; + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (mChangeAlphaWhenDisabled) { + if (mEnabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public boolean isEnabled() { + return mEnabled; + } + + public void setSelected(boolean s) { + mSelected = s; + } + + public boolean isSelected() { + return mSelected; + } + + public int getLevel() { + return level; + } + + public void setGeometry(float st, float sw, int inside, int outside) { + start = st; + sweep = sw; + inner = inside; + outer = outside; + } + + public void setFixedSlice(float center, float sweep) { + mCenter = center; + this.sweep = sweep; + } + + public float getCenter() { + return mCenter; + } + + public float getStart() { + return start; + } + + public float getStartAngle() { + return start + animate; + } + + public float getSweep() { + return sweep; + } + + public int getInnerRadius() { + return inner; + } + + public int getOuterRadius() { + return outer; + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + public void performClick() { + if (mOnClickListener != null) { + mOnClickListener.onClick(this); + } + } + + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + public void setBounds(int left, int top, int right, int bottom) { + mDrawable.setBounds(left, top, right, bottom); + } + + public void draw(Canvas canvas) { + mDrawable.draw(canvas); + } + + public void setImageResource(Context context, int resId) { + Drawable d = context.getResources().getDrawable(resId).mutate(); + d.setBounds(mDrawable.getBounds()); + mDrawable = d; + setAlpha(mAlpha); + } + +} diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java new file mode 100644 index 000000000..b592508e1 --- /dev/null +++ b/src/com/android/camera/ui/PieRenderer.java @@ -0,0 +1,825 @@ +/* + * 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.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Message; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; + +import com.android.camera.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.List; + +public class PieRenderer extends OverlayRenderer + implements FocusIndicator { + + private static final String TAG = "CAM Pie"; + + // Sometimes continuous autofocus starts and stops several times quickly. + // These states are used to make sure the animation is run for at least some + // time. + private volatile int mState; + private ScaleAnimation mAnimation = new ScaleAnimation(); + private static final int STATE_IDLE = 0; + private static final int STATE_FOCUSING = 1; + private static final int STATE_FINISHING = 2; + private static final int STATE_PIE = 8; + + private Runnable mDisappear = new Disappear(); + private Animation.AnimationListener mEndAction = new EndAction(); + private static final int SCALING_UP_TIME = 600; + private static final int SCALING_DOWN_TIME = 100; + private static final int DISAPPEAR_TIMEOUT = 200; + private static final int DIAL_HORIZONTAL = 157; + + private static final long PIE_FADE_IN_DURATION = 200; + private static final long PIE_XFADE_DURATION = 200; + private static final long PIE_SELECT_FADE_DURATION = 300; + + private static final int MSG_OPEN = 0; + private static final int MSG_CLOSE = 1; + private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); + // geometry + private Point mCenter; + private int mRadius; + private int mRadiusInc; + + // the detection if touch is inside a slice is offset + // inbounds by this amount to allow the selection to show before the + // finger covers it + private int mTouchOffset; + + private List mItems; + + private PieItem mOpenItem; + + private Paint mSelectedPaint; + private Paint mSubPaint; + + // touch handling + private PieItem mCurrentItem; + + private Paint mFocusPaint; + private int mSuccessColor; + private int mFailColor; + private int mCircleSize; + private int mFocusX; + private int mFocusY; + private int mCenterX; + private int mCenterY; + + private int mDialAngle; + private RectF mCircle; + private RectF mDial; + private Point mPoint1; + private Point mPoint2; + private int mStartAnimationAngle; + private boolean mFocused; + private int mInnerOffset; + private int mOuterStroke; + private int mInnerStroke; + private boolean mTapMode; + private boolean mBlockFocus; + private int mTouchSlopSquared; + private Point mDown; + private boolean mOpening; + private LinearAnimation mXFade; + private LinearAnimation mFadeIn; + private volatile boolean mFocusCancelled; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_OPEN: + if (mListener != null) { + mListener.onPieOpened(mCenter.x, mCenter.y); + } + break; + case MSG_CLOSE: + if (mListener != null) { + mListener.onPieClosed(); + } + break; + } + } + }; + + private PieListener mListener; + + static public interface PieListener { + public void onPieOpened(int centerX, int centerY); + public void onPieClosed(); + } + + public void setPieListener(PieListener pl) { + mListener = pl; + } + + public PieRenderer(Context context) { + init(context); + } + + private void init(Context ctx) { + setVisible(false); + mItems = new ArrayList(); + Resources res = ctx.getResources(); + mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); + mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); + mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); + mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); + mCenter = new Point(0,0); + mSelectedPaint = new Paint(); + mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); + mSelectedPaint.setAntiAlias(true); + mSubPaint = new Paint(); + mSubPaint.setAntiAlias(true); + mSubPaint.setColor(Color.argb(200, 250, 230, 128)); + mFocusPaint = new Paint(); + mFocusPaint.setAntiAlias(true); + mFocusPaint.setColor(Color.WHITE); + mFocusPaint.setStyle(Paint.Style.STROKE); + mSuccessColor = Color.GREEN; + mFailColor = Color.RED; + mCircle = new RectF(); + mDial = new RectF(); + mPoint1 = new Point(); + mPoint2 = new Point(); + mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mState = STATE_IDLE; + mBlockFocus = false; + mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); + mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared; + mDown = new Point(); + } + + public boolean showsItems() { + return mTapMode; + } + + public void addItem(PieItem item) { + // add the item to the pie itself + mItems.add(item); + } + + public void removeItem(PieItem item) { + mItems.remove(item); + } + + public void clearItems() { + mItems.clear(); + } + + public void showInCenter() { + if ((mState == STATE_PIE) && isVisible()) { + mTapMode = false; + show(false); + } else { + if (mState != STATE_IDLE) { + cancelFocus(); + } + mState = STATE_PIE; + setCenter(mCenterX, mCenterY); + mTapMode = true; + show(true); + } + } + + public void hide() { + show(false); + } + + /** + * guaranteed has center set + * @param show + */ + private void show(boolean show) { + if (show) { + mState = STATE_PIE; + // ensure clean state + mCurrentItem = null; + mOpenItem = null; + for (PieItem item : mItems) { + item.setSelected(false); + } + layoutPie(); + fadeIn(); + } else { + mState = STATE_IDLE; + mTapMode = false; + if (mXFade != null) { + mXFade.cancel(); + } + } + setVisible(show); + mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); + } + + private void fadeIn() { + mFadeIn = new LinearAnimation(0, 1); + mFadeIn.setDuration(PIE_FADE_IN_DURATION); + mFadeIn.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mFadeIn = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + mFadeIn.startNow(); + mOverlay.startAnimation(mFadeIn); + } + + public void setCenter(int x, int y) { + mCenter.x = x; + mCenter.y = y; + // when using the pie menu, align the focus ring + alignFocus(x, y); + } + + private void layoutPie() { + int rgap = 2; + int inner = mRadius + rgap; + int outer = mRadius + mRadiusInc - rgap; + int gap = 1; + layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); + } + + private void layoutItems(List items, float centerAngle, int inner, + int outer, int gap) { + float emptyangle = PIE_SWEEP / 16; + float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); + float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; + // check if we have custom geometry + // first item we find triggers custom sweep for all + // this allows us to re-use the path + for (PieItem item : items) { + if (item.getCenter() >= 0) { + sweep = item.getSweep(); + break; + } + } + Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, + outer, inner, mCenter); + for (PieItem item : items) { + // shared between items + item.setPath(path); + if (item.getCenter() >= 0) { + angle = item.getCenter(); + } + int w = item.getIntrinsicWidth(); + int h = item.getIntrinsicHeight(); + // move views to outer border + int r = inner + (outer - inner) * 2 / 3; + int x = (int) (r * Math.cos(angle)); + int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; + x = mCenter.x + x - w / 2; + item.setBounds(x, y, x + w, y + h); + float itemstart = angle - sweep / 2; + item.setGeometry(itemstart, sweep, inner, outer); + if (item.hasItems()) { + layoutItems(item.getItems(), angle, inner, + outer + mRadiusInc / 2, gap); + } + angle += sweep; + } + } + + private Path makeSlice(float start, float end, int outer, int inner, Point center) { + RectF bb = + new RectF(center.x - outer, center.y - outer, center.x + outer, + center.y + outer); + RectF bbi = + new RectF(center.x - inner, center.y - inner, center.x + inner, + center.y + inner); + Path path = new Path(); + path.arcTo(bb, start, end - start, true); + path.arcTo(bbi, end, start - end); + path.close(); + return path; + } + + /** + * converts a + * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) + * @return skia angle + */ + private float getDegrees(double angle) { + return (float) (360 - 180 * angle / Math.PI); + } + + private void startFadeOut() { + if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + deselect(); + show(false); + mOverlay.setAlpha(1); + super.onAnimationEnd(animation); + } + }).setDuration(PIE_SELECT_FADE_DURATION); + } else { + deselect(); + show(false); + } + } + + @Override + public void onDraw(Canvas canvas) { + float alpha = 1; + if (mXFade != null) { + alpha = mXFade.getValue(); + } else if (mFadeIn != null) { + alpha = mFadeIn.getValue(); + } + int state = canvas.save(); + if (mFadeIn != null) { + float sf = 0.9f + alpha * 0.1f; + canvas.scale(sf, sf, mCenter.x, mCenter.y); + } + drawFocus(canvas); + if (mState == STATE_FINISHING) { + canvas.restoreToCount(state); + return; + } + if ((mOpenItem == null) || (mXFade != null)) { + // draw base menu + for (PieItem item : mItems) { + drawItem(canvas, item, alpha); + } + } + if (mOpenItem != null) { + for (PieItem inner : mOpenItem.getItems()) { + drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); + } + } + canvas.restoreToCount(state); + } + + private void drawItem(Canvas canvas, PieItem item, float alpha) { + if (mState == STATE_PIE) { + if (item.getPath() != null) { + if (item.isSelected()) { + Paint p = mSelectedPaint; + int state = canvas.save(); + float r = getDegrees(item.getStartAngle()); + canvas.rotate(r, mCenter.x, mCenter.y); + canvas.drawPath(item.getPath(), p); + canvas.restoreToCount(state); + } + alpha = alpha * (item.isEnabled() ? 1 : 0.3f); + // draw the item view + item.setAlpha(alpha); + item.draw(canvas); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + float x = evt.getX(); + float y = evt.getY(); + int action = evt.getActionMasked(); + PointF polar = getPolar(x, y, !(mTapMode)); + if (MotionEvent.ACTION_DOWN == action) { + mDown.x = (int) evt.getX(); + mDown.y = (int) evt.getY(); + mOpening = false; + if (mTapMode) { + PieItem item = findItem(polar); + if ((item != null) && (mCurrentItem != item)) { + mState = STATE_PIE; + onEnter(item); + } + } else { + setCenter((int) x, (int) y); + show(true); + } + return true; + } else if (MotionEvent.ACTION_UP == action) { + if (isVisible()) { + PieItem item = mCurrentItem; + if (mTapMode) { + item = findItem(polar); + if (item != null && mOpening) { + mOpening = false; + return true; + } + } + if (item == null) { + mTapMode = false; + show(false); + } else if (!mOpening + && !item.hasItems()) { + item.performClick(); + startFadeOut(); + mTapMode = false; + } + return true; + } + } else if (MotionEvent.ACTION_CANCEL == action) { + if (isVisible() || mTapMode) { + show(false); + } + deselect(); + return false; + } else if (MotionEvent.ACTION_MOVE == action) { + if (polar.y < mRadius) { + if (mOpenItem != null) { + mOpenItem = null; + } else { + deselect(); + } + return false; + } + PieItem item = findItem(polar); + boolean moved = hasMoved(evt); + if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { + // only select if we didn't just open or have moved past slop + mOpening = false; + if (moved) { + // switch back to swipe mode + mTapMode = false; + } + onEnter(item); + } + } + return false; + } + + private boolean hasMoved(MotionEvent e) { + return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) + + (e.getY() - mDown.y) * (e.getY() - mDown.y); + } + + /** + * enter a slice for a view + * updates model only + * @param item + */ + private void onEnter(PieItem item) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (item != null && item.isEnabled()) { + item.setSelected(true); + mCurrentItem = item; + if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { + openCurrentItem(); + } + } else { + mCurrentItem = null; + } + } + + private void deselect() { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (mOpenItem != null) { + mOpenItem = null; + } + mCurrentItem = null; + } + + private void openCurrentItem() { + if ((mCurrentItem != null) && mCurrentItem.hasItems()) { + mCurrentItem.setSelected(false); + mOpenItem = mCurrentItem; + mOpening = true; + mXFade = new LinearAnimation(1, 0); + mXFade.setDuration(PIE_XFADE_DURATION); + mXFade.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mXFade = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + mXFade.startNow(); + mOverlay.startAnimation(mXFade); + } + } + + private PointF getPolar(float x, float y, boolean useOffset) { + PointF res = new PointF(); + // get angle and radius from x/y + res.x = (float) Math.PI / 2; + x = x - mCenter.x; + y = mCenter.y - y; + res.y = (float) Math.sqrt(x * x + y * y); + if (x != 0) { + res.x = (float) Math.atan2(y, x); + if (res.x < 0) { + res.x = (float) (2 * Math.PI + res.x); + } + } + res.y = res.y + (useOffset ? mTouchOffset : 0); + return res; + } + + /** + * @param polar x: angle, y: dist + * @return the item at angle/dist or null + */ + private PieItem findItem(PointF polar) { + // find the matching item: + List items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; + for (PieItem item : items) { + if (inside(polar, item)) { + return item; + } + } + return null; + } + + private boolean inside(PointF polar, PieItem item) { + return (item.getInnerRadius() < polar.y) + && (item.getStartAngle() < polar.x) + && (item.getStartAngle() + item.getSweep() > polar.x) + && (!mTapMode || (item.getOuterRadius() > polar.y)); + } + + @Override + public boolean handlesTouch() { + return true; + } + + // focus specific code + + public void setBlockFocus(boolean blocked) { + mBlockFocus = blocked; + if (blocked) { + clear(); + } + } + + public void setFocus(int x, int y) { + mFocusX = x; + mFocusY = y; + setCircle(mFocusX, mFocusY); + } + + public void alignFocus(int x, int y) { + mOverlay.removeCallbacks(mDisappear); + mAnimation.cancel(); + mAnimation.reset(); + mFocusX = x; + mFocusY = y; + mDialAngle = DIAL_HORIZONTAL; + setCircle(x, y); + mFocused = false; + } + + public int getSize() { + return 2 * mCircleSize; + } + + private int getRandomRange() { + return (int)(-60 + 120 * Math.random()); + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + mFocusX = mCenterX; + mFocusY = mCenterY; + setCircle(mFocusX, mFocusY); + if (isVisible() && mState == STATE_PIE) { + setCenter(mCenterX, mCenterY); + layoutPie(); + } + } + + private void setCircle(int cx, int cy) { + mCircle.set(cx - mCircleSize, cy - mCircleSize, + cx + mCircleSize, cy + mCircleSize); + mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, + cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); + } + + public void drawFocus(Canvas canvas) { + if (mBlockFocus) return; + mFocusPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); + if (mState == STATE_PIE) return; + int color = mFocusPaint.getColor(); + if (mState == STATE_FINISHING) { + mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); + } + mFocusPaint.setStrokeWidth(mInnerStroke); + drawLine(canvas, mDialAngle, mFocusPaint); + drawLine(canvas, mDialAngle + 45, mFocusPaint); + drawLine(canvas, mDialAngle + 180, mFocusPaint); + drawLine(canvas, mDialAngle + 225, mFocusPaint); + canvas.save(); + // rotate the arc instead of its offset to better use framework's shape caching + canvas.rotate(mDialAngle, mFocusX, mFocusY); + canvas.drawArc(mDial, 0, 45, false, mFocusPaint); + canvas.drawArc(mDial, 180, 45, false, mFocusPaint); + canvas.restore(); + mFocusPaint.setColor(color); + } + + private void drawLine(Canvas canvas, int angle, Paint p) { + convertCart(angle, mCircleSize - mInnerOffset, mPoint1); + convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); + canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, + mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); + } + + private static void convertCart(int angle, int radius, Point out) { + double a = 2 * Math.PI * (angle % 360) / 360; + out.x = (int) (radius * Math.cos(a) + 0.5); + out.y = (int) (radius * Math.sin(a) + 0.5); + } + + @Override + public void showStart() { + if (mState == STATE_PIE) return; + cancelFocus(); + mStartAnimationAngle = 67; + int range = getRandomRange(); + startAnimation(SCALING_UP_TIME, + false, mStartAnimationAngle, mStartAnimationAngle + range); + mState = STATE_FOCUSING; + } + + @Override + public void showSuccess(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = true; + } + } + + @Override + public void showFail(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = false; + } + } + + private void cancelFocus() { + mFocusCancelled = true; + mOverlay.removeCallbacks(mDisappear); + if (mAnimation != null) { + mAnimation.cancel(); + } + mFocusCancelled = false; + mFocused = false; + mState = STATE_IDLE; + } + + @Override + public void clear() { + if (mState == STATE_PIE) return; + cancelFocus(); + mOverlay.post(mDisappear); + } + + private void startAnimation(long duration, boolean timeout, + float toScale) { + startAnimation(duration, timeout, mDialAngle, + toScale); + } + + private void startAnimation(long duration, boolean timeout, + float fromScale, float toScale) { + setVisible(true); + mAnimation.reset(); + mAnimation.setDuration(duration); + mAnimation.setScale(fromScale, toScale); + mAnimation.setAnimationListener(timeout ? mEndAction : null); + mOverlay.startAnimation(mAnimation); + update(); + } + + private class EndAction implements Animation.AnimationListener { + @Override + public void onAnimationEnd(Animation animation) { + // Keep the focus indicator for some time. + if (!mFocusCancelled) { + mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + private class Disappear implements Runnable { + @Override + public void run() { + if (mState == STATE_PIE) return; + setVisible(false); + mFocusX = mCenterX; + mFocusY = mCenterY; + mState = STATE_IDLE; + setCircle(mFocusX, mFocusY); + mFocused = false; + } + } + + private class ScaleAnimation extends Animation { + private float mFrom = 1f; + private float mTo = 1f; + + public ScaleAnimation() { + setFillAfter(true); + } + + public void setScale(float from, float to) { + mFrom = from; + mTo = to; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); + } + } + + + private class LinearAnimation extends Animation { + private float mFrom; + private float mTo; + private float mValue; + + public LinearAnimation(float from, float to) { + setFillAfter(true); + setInterpolator(new LinearInterpolator()); + mFrom = from; + mTo = to; + } + + public float getValue() { + return mValue; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mValue = (mFrom + (mTo - mFrom) * interpolatedTime); + } + } + +} diff --git a/src/com/android/camera/ui/PopupManager.java b/src/com/android/camera/ui/PopupManager.java new file mode 100644 index 000000000..0dcf34fd7 --- /dev/null +++ b/src/com/android/camera/ui/PopupManager.java @@ -0,0 +1,66 @@ +/* + * 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.ui; + +import android.content.Context; +import android.view.View; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A manager which notifies the event of a new popup in order to dismiss the + * old popup if exists. + */ +public class PopupManager { + private static HashMap sMap = + new HashMap(); + + public interface OnOtherPopupShowedListener { + public void onOtherPopupShowed(); + } + + private PopupManager() {} + + private ArrayList mListeners = new ArrayList(); + + public void notifyShowPopup(View view) { + for (OnOtherPopupShowedListener listener : mListeners) { + if ((View) listener != view) { + listener.onOtherPopupShowed(); + } + } + } + + public void setOnOtherPopupShowedListener(OnOtherPopupShowedListener listener) { + mListeners.add(listener); + } + + public static PopupManager getInstance(Context context) { + PopupManager instance = sMap.get(context); + if (instance == null) { + instance = new PopupManager(); + sMap.put(context, instance); + } + return instance; + } + + public static void removeInstance(Context context) { + PopupManager instance = sMap.get(context); + sMap.remove(context); + } +} diff --git a/src/com/android/camera/ui/PreviewSurfaceView.java b/src/com/android/camera/ui/PreviewSurfaceView.java new file mode 100644 index 000000000..9a428e23c --- /dev/null +++ b/src/com/android/camera/ui/PreviewSurfaceView.java @@ -0,0 +1,50 @@ +/* + * 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.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.android.gallery3d.common.ApiHelper; + +public class PreviewSurfaceView extends SurfaceView { + public PreviewSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + setZOrderMediaOverlay(true); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + public void shrink() { + setLayoutSize(1); + } + + public void expand() { + setLayoutSize(ViewGroup.LayoutParams.MATCH_PARENT); + } + + private void setLayoutSize(int size) { + ViewGroup.LayoutParams p = getLayoutParams(); + if (p.width != size || p.height != size) { + p.width = size; + p.height = size; + setLayoutParams(p); + } + } +} diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java new file mode 100644 index 000000000..ba2591511 --- /dev/null +++ b/src/com/android/camera/ui/RenderOverlay.java @@ -0,0 +1,165 @@ +/* + * 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.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +public class RenderOverlay extends FrameLayout { + + private static final String TAG = "CAM_Overlay"; + + interface Renderer { + + public boolean handlesTouch(); + public boolean onTouchEvent(MotionEvent evt); + public void setOverlay(RenderOverlay overlay); + public void layout(int left, int top, int right, int bottom); + public void draw(Canvas canvas); + + } + + private RenderView mRenderView; + private List mClients; + + // reverse list of touch clients + private List mTouchClients; + private int[] mPosition = new int[2]; + + public RenderOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + mRenderView = new RenderView(context); + addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + mClients = new ArrayList(10); + mTouchClients = new ArrayList(10); + setWillNotDraw(false); + } + + public void addRenderer(Renderer renderer) { + mClients.add(renderer); + renderer.setOverlay(this); + if (renderer.handlesTouch()) { + mTouchClients.add(0, renderer); + } + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void addRenderer(int pos, Renderer renderer) { + mClients.add(pos, renderer); + renderer.setOverlay(this); + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void remove(Renderer renderer) { + mClients.remove(renderer); + renderer.setOverlay(null); + } + + public int getClientSize() { + return mClients.size(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + return false; + } + + public boolean directDispatchTouch(MotionEvent m, Renderer target) { + mRenderView.setTouchTarget(target); + boolean res = super.dispatchTouchEvent(m); + mRenderView.setTouchTarget(null); + return res; + } + + private void adjustPosition() { + getLocationInWindow(mPosition); + } + + public int getWindowPositionX() { + return mPosition[0]; + } + + public int getWindowPositionY() { + return mPosition[1]; + } + + public void update() { + mRenderView.invalidate(); + } + + private class RenderView extends View { + + private Renderer mTouchTarget; + + public RenderView(Context context) { + super(context); + setWillNotDraw(false); + } + + public void setTouchTarget(Renderer target) { + mTouchTarget = target; + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + if (mTouchTarget != null) { + return mTouchTarget.onTouchEvent(evt); + } + if (mTouchClients != null) { + boolean res = false; + for (Renderer client : mTouchClients) { + res |= client.onTouchEvent(evt); + } + return res; + } + return false; + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + adjustPosition(); + super.onLayout(changed, left, top, right, bottom); + if (mClients == null) return; + for (Renderer renderer : mClients) { + renderer.layout(left, top, right, bottom); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mClients == null) return; + boolean redraw = false; + for (Renderer renderer : mClients) { + renderer.draw(canvas); + redraw = redraw || ((OverlayRenderer) renderer).isVisible(); + } + if (redraw) { + invalidate(); + } + } + } + +} diff --git a/src/com/android/camera/ui/Rotatable.java b/src/com/android/camera/ui/Rotatable.java new file mode 100644 index 000000000..6d428b8c6 --- /dev/null +++ b/src/com/android/camera/ui/Rotatable.java @@ -0,0 +1,22 @@ +/* + * 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.ui; + +public interface Rotatable { + // Set parameter 'animation' to true to have animation when rotation. + public void setOrientation(int orientation, boolean animation); +} diff --git a/src/com/android/camera/ui/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java new file mode 100644 index 000000000..05e1a7c5b --- /dev/null +++ b/src/com/android/camera/ui/RotateImageView.java @@ -0,0 +1,176 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.ThumbnailUtils; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; + +/** + * A @{code ImageView} which can rotate it's content. + */ +public class RotateImageView extends TwoStateImageView implements Rotatable { + + @SuppressWarnings("unused") + private static final String TAG = "RotateImageView"; + + private static final int ANIMATION_SPEED = 270; // 270 deg/sec + + private int mCurrentDegree = 0; // [0, 359] + private int mStartDegree = 0; + private int mTargetDegree = 0; + + private boolean mClockwise = false, mEnableAnimation = true; + + private long mAnimationStartTime = 0; + private long mAnimationEndTime = 0; + + public RotateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RotateImageView(Context context) { + super(context); + } + + protected int getDegree() { + return mTargetDegree; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int degree, boolean animation) { + mEnableAnimation = animation; + // make sure in the range of [0, 359] + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + if (degree == mTargetDegree) return; + + mTargetDegree = degree; + if (mEnableAnimation) { + mStartDegree = mCurrentDegree; + mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis(); + + int diff = mTargetDegree - mCurrentDegree; + diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359] + + // Make it in range [-179, 180]. That's the shorted distance between the + // two angles + diff = diff > 180 ? diff - 360 : diff; + + mClockwise = diff >= 0; + mAnimationEndTime = mAnimationStartTime + + Math.abs(diff) * 1000 / ANIMATION_SPEED; + } else { + mCurrentDegree = mTargetDegree; + } + + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable drawable = getDrawable(); + if (drawable == null) return; + + Rect bounds = drawable.getBounds(); + int w = bounds.right - bounds.left; + int h = bounds.bottom - bounds.top; + + if (w == 0 || h == 0) return; // nothing to draw + + if (mCurrentDegree != mTargetDegree) { + long time = AnimationUtils.currentAnimationTimeMillis(); + if (time < mAnimationEndTime) { + int deltaTime = (int)(time - mAnimationStartTime); + int degree = mStartDegree + ANIMATION_SPEED + * (mClockwise ? deltaTime : -deltaTime) / 1000; + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + mCurrentDegree = degree; + invalidate(); + } else { + mCurrentDegree = mTargetDegree; + } + } + + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getPaddingRight(); + int bottom = getPaddingBottom(); + int width = getWidth() - left - right; + int height = getHeight() - top - bottom; + + int saveCount = canvas.getSaveCount(); + + // Scale down the image first if required. + if ((getScaleType() == ImageView.ScaleType.FIT_CENTER) && + ((width < w) || (height < h))) { + float ratio = Math.min((float) width / w, (float) height / h); + canvas.scale(ratio, ratio, width / 2.0f, height / 2.0f); + } + canvas.translate(left + width / 2, top + height / 2); + canvas.rotate(-mCurrentDegree); + canvas.translate(-w / 2, -h / 2); + drawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + private Bitmap mThumb; + private Drawable[] mThumbs; + private TransitionDrawable mThumbTransition; + + public void setBitmap(Bitmap bitmap) { + // Make sure uri and original are consistently both null or both + // non-null. + if (bitmap == null) { + mThumb = null; + mThumbs = null; + setImageDrawable(null); + setVisibility(GONE); + return; + } + + LayoutParams param = getLayoutParams(); + final int miniThumbWidth = param.width + - getPaddingLeft() - getPaddingRight(); + final int miniThumbHeight = param.height + - getPaddingTop() - getPaddingBottom(); + mThumb = ThumbnailUtils.extractThumbnail( + bitmap, miniThumbWidth, miniThumbHeight); + Drawable drawable; + if (mThumbs == null || !mEnableAnimation) { + mThumbs = new Drawable[2]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + setImageDrawable(mThumbs[1]); + } else { + mThumbs[0] = mThumbs[1]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + mThumbTransition = new TransitionDrawable(mThumbs); + setImageDrawable(mThumbTransition); + mThumbTransition.startTransition(500); + } + setVisibility(VISIBLE); + } +} diff --git a/src/com/android/camera/ui/RotateLayout.java b/src/com/android/camera/ui/RotateLayout.java new file mode 100644 index 000000000..86f5c814d --- /dev/null +++ b/src/com/android/camera/ui/RotateLayout.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.MotionEventHelper; + +// A RotateLayout is designed to display a single item and provides the +// capabilities to rotate the item. +public class RotateLayout extends ViewGroup implements Rotatable { + @SuppressWarnings("unused") + private static final String TAG = "RotateLayout"; + private int mOrientation; + private Matrix mMatrix = new Matrix(); + protected View mChild; + + public RotateLayout(Context context, AttributeSet attrs) { + super(context, attrs); + // The transparent background here is a workaround of the render issue + // happened when the view is rotated as the device's orientation + // changed. The view looks fine in landscape. After rotation, the view + // is invisible. + setBackgroundResource(android.R.color.transparent); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onFinishInflate() { + mChild = getChildAt(0); + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + mChild.setPivotX(0); + mChild.setPivotY(0); + } + } + + @Override + protected void onLayout( + boolean change, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + switch (mOrientation) { + case 0: + case 180: + mChild.layout(0, 0, width, height); + break; + case 90: + case 270: + mChild.layout(0, 0, height, width); + break; + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + final int w = getMeasuredWidth(); + final int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + mMatrix.setTranslate(0, 0); + break; + case 90: + mMatrix.setTranslate(0, -h); + break; + case 180: + mMatrix.setTranslate(-w, -h); + break; + case 270: + mMatrix.setTranslate(-w, 0); + break; + } + mMatrix.postRotate(mOrientation); + event = MotionEventHelper.transformEvent(event, mMatrix); + } + return super.dispatchTouchEvent(event); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + super.dispatchDraw(canvas); + } else { + canvas.save(); + int w = getMeasuredWidth(); + int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + canvas.translate(0, 0); + break; + case 90: + canvas.translate(0, h); + break; + case 180: + canvas.translate(w, h); + break; + case 270: + canvas.translate(w, 0); + break; + } + canvas.rotate(-mOrientation, 0, 0); + super.dispatchDraw(canvas); + canvas.restore(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int w = 0, h = 0; + switch(mOrientation) { + case 0: + case 180: + measureChild(mChild, widthSpec, heightSpec); + w = mChild.getMeasuredWidth(); + h = mChild.getMeasuredHeight(); + break; + case 90: + case 270: + measureChild(mChild, heightSpec, widthSpec); + w = mChild.getMeasuredHeight(); + h = mChild.getMeasuredWidth(); + break; + } + setMeasuredDimension(w, h); + + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + switch (mOrientation) { + case 0: + mChild.setTranslationX(0); + mChild.setTranslationY(0); + break; + case 90: + mChild.setTranslationX(0); + mChild.setTranslationY(h); + break; + case 180: + mChild.setTranslationX(w); + mChild.setTranslationY(h); + break; + case 270: + mChild.setTranslationX(w); + mChild.setTranslationY(0); + break; + } + mChild.setRotation(-mOrientation); + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int orientation, boolean animation) { + orientation = orientation % 360; + if (mOrientation == orientation) return; + mOrientation = orientation; + requestLayout(); + } + + public int getOrientation() { + return mOrientation; + } + + @Override + public ViewParent invalidateChildInParent(int[] location, Rect r) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES && mOrientation != 0) { + // The workaround invalidates the entire rotate layout. After + // rotation, the correct area to invalidate may be larger than the + // size of the child. Ex: ListView. There is no way to invalidate + // only the necessary area. + r.set(0, 0, getWidth(), getHeight()); + } + return super.invalidateChildInParent(location, r); + } +} diff --git a/src/com/android/camera/ui/RotateTextToast.java b/src/com/android/camera/ui/RotateTextToast.java new file mode 100644 index 000000000..f73c03362 --- /dev/null +++ b/src/com/android/camera/ui/RotateTextToast.java @@ -0,0 +1,59 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.camera.R; +import com.android.camera.Util; + +public class RotateTextToast { + private static final int TOAST_DURATION = 5000; // milliseconds + ViewGroup mLayoutRoot; + RotateLayout mToast; + Handler mHandler; + + public RotateTextToast(Activity activity, int textResourceId, int orientation) { + mLayoutRoot = (ViewGroup) activity.getWindow().getDecorView(); + LayoutInflater inflater = activity.getLayoutInflater(); + View v = inflater.inflate(R.layout.rotate_text_toast, mLayoutRoot); + mToast = (RotateLayout) v.findViewById(R.id.rotate_toast); + TextView tv = (TextView) mToast.findViewById(R.id.message); + tv.setText(textResourceId); + mToast.setOrientation(orientation, false); + mHandler = new Handler(); + } + + private final Runnable mRunnable = new Runnable() { + @Override + public void run() { + Util.fadeOut(mToast); + mLayoutRoot.removeView(mToast); + mToast = null; + } + }; + + public void show() { + mToast.setVisibility(View.VISIBLE); + mHandler.postDelayed(mRunnable, TOAST_DURATION); + } +} diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java new file mode 100644 index 000000000..5b1ab4c97 --- /dev/null +++ b/src/com/android/camera/ui/Switch.java @@ -0,0 +1,505 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.CompoundButton; + +import com.android.camera.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.Arrays; + +/** + * A Switch is a two-state toggle switch widget that can select between two + * options. The user may drag the "thumb" back and forth to choose the selected option, + * or simply tap to toggle as if it were a checkbox. + */ +public class Switch extends CompoundButton { + private static final int TOUCH_MODE_IDLE = 0; + private static final int TOUCH_MODE_DOWN = 1; + private static final int TOUCH_MODE_DRAGGING = 2; + + private Drawable mThumbDrawable; + private Drawable mTrackDrawable; + private int mThumbTextPadding; + private int mSwitchMinWidth; + private int mSwitchTextMaxWidth; + private int mSwitchPadding; + private CharSequence mTextOn; + private CharSequence mTextOff; + + private int mTouchMode; + private int mTouchSlop; + private float mTouchX; + private float mTouchY; + private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private int mMinFlingVelocity; + + private float mThumbPosition; + private int mSwitchWidth; + private int mSwitchHeight; + private int mThumbWidth; // Does not include padding + + private int mSwitchLeft; + private int mSwitchTop; + private int mSwitchRight; + private int mSwitchBottom; + + private TextPaint mTextPaint; + private ColorStateList mTextColors; + private Layout mOnLayout; + private Layout mOffLayout; + + @SuppressWarnings("hiding") + private final Rect mTempRect = new Rect(); + + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + + /** + * Construct a new Switch with default styling, overriding specific style + * attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from default styling. + */ + public Switch(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.switchStyle); + } + + /** + * Construct a new Switch with a default style determined by the given theme attribute, + * overriding specific style attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from the default styling. + * @param defStyle An attribute ID within the active theme containing a reference to the + * default style for this widget. e.g. android.R.attr.switchStyle. + */ + public Switch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + Resources res = getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + mTextPaint.density = dm.density; + mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark); + mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark); + mTextOn = res.getString(R.string.capital_on); + mTextOff = res.getString(R.string.capital_off); + mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding); + mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width); + mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width); + mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding); + setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small); + + ViewConfiguration config = ViewConfiguration.get(context); + mTouchSlop = config.getScaledTouchSlop(); + mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); + + // Refresh display with current params + refreshDrawableState(); + setChecked(isChecked()); + } + + /** + * Sets the switch text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setSwitchTextAppearance(Context context, int resid) { + Resources res = getResources(); + mTextColors = getTextColors(); + int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size); + if (ts != mTextPaint.getTextSize()) { + mTextPaint.setTextSize(ts); + requestLayout(); + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (mOnLayout == null) { + mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth); + } + if (mOffLayout == null) { + mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth); + } + + mTrackDrawable.getPadding(mTempRect); + final int maxTextWidth = Math.min(mSwitchTextMaxWidth, + Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())); + final int switchWidth = Math.max(mSwitchMinWidth, + maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); + final int switchHeight = mTrackDrawable.getIntrinsicHeight(); + + mThumbWidth = maxTextWidth + mThumbTextPadding * 2; + + mSwitchWidth = switchWidth; + mSwitchHeight = switchHeight; + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int measuredHeight = getMeasuredHeight(); + final int measuredWidth = getMeasuredWidth(); + if (measuredHeight < switchHeight) { + setMeasuredDimension(measuredWidth, switchHeight); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText(); + if (!TextUtils.isEmpty(text)) { + event.getText().add(text); + } + } + + private Layout makeLayout(CharSequence text, int maxWidth) { + int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)); + StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint, + actual_width, + Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true, + TextUtils.TruncateAt.END, + (int) Math.min(actual_width, maxWidth)); + return l; + } + + /** + * @return true if (x, y) is within the target area of the switch thumb + */ + private boolean hitThumb(float x, float y) { + mThumbDrawable.getPadding(mTempRect); + final int thumbTop = mSwitchTop - mTouchSlop; + final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop; + final int thumbRight = thumbLeft + mThumbWidth + + mTempRect.left + mTempRect.right + mTouchSlop; + final int thumbBottom = mSwitchBottom + mTouchSlop; + return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + final int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (isEnabled() && hitThumb(x, y)) { + mTouchMode = TOUCH_MODE_DOWN; + mTouchX = x; + mTouchY = y; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_IDLE: + // Didn't target the thumb, treat normally. + break; + + case TOUCH_MODE_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (Math.abs(x - mTouchX) > mTouchSlop || + Math.abs(y - mTouchY) > mTouchSlop) { + mTouchMode = TOUCH_MODE_DRAGGING; + getParent().requestDisallowInterceptTouchEvent(true); + mTouchX = x; + mTouchY = y; + return true; + } + break; + } + + case TOUCH_MODE_DRAGGING: { + final float x = ev.getX(); + final float dx = x - mTouchX; + float newPos = Math.max(0, + Math.min(mThumbPosition + dx, getThumbScrollRange())); + if (newPos != mThumbPosition) { + mThumbPosition = newPos; + mTouchX = x; + invalidate(); + } + return true; + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mTouchMode == TOUCH_MODE_DRAGGING) { + stopDrag(ev); + return true; + } + mTouchMode = TOUCH_MODE_IDLE; + mVelocityTracker.clear(); + break; + } + } + + return super.onTouchEvent(ev); + } + + private void cancelSuperTouch(MotionEvent ev) { + MotionEvent cancel = MotionEvent.obtain(ev); + cancel.setAction(MotionEvent.ACTION_CANCEL); + super.onTouchEvent(cancel); + cancel.recycle(); + } + + /** + * Called from onTouchEvent to end a drag operation. + * + * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL + */ + private void stopDrag(MotionEvent ev) { + mTouchMode = TOUCH_MODE_IDLE; + // Up and not canceled, also checks the switch has not been disabled during the drag + boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + + cancelSuperTouch(ev); + + if (commitChange) { + boolean newState; + mVelocityTracker.computeCurrentVelocity(1000); + float xvel = mVelocityTracker.getXVelocity(); + if (Math.abs(xvel) > mMinFlingVelocity) { + newState = xvel > 0; + } else { + newState = getTargetCheckedState(); + } + animateThumbToCheckedState(newState); + } else { + animateThumbToCheckedState(isChecked()); + } + } + + private void animateThumbToCheckedState(boolean newCheckedState) { + setChecked(newCheckedState); + } + + private boolean getTargetCheckedState() { + return mThumbPosition >= getThumbScrollRange() / 2; + } + + private void setThumbPosition(boolean checked) { + mThumbPosition = checked ? getThumbScrollRange() : 0; + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + setThumbPosition(checked); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + setThumbPosition(isChecked()); + + int switchRight; + int switchLeft; + + switchRight = getWidth() - getPaddingRight(); + switchLeft = switchRight - mSwitchWidth; + + int switchTop = 0; + int switchBottom = 0; + switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { + default: + case Gravity.TOP: + switchTop = getPaddingTop(); + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.CENTER_VERTICAL: + switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - + mSwitchHeight / 2; + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.BOTTOM: + switchBottom = getHeight() - getPaddingBottom(); + switchTop = switchBottom - mSwitchHeight; + break; + } + + mSwitchLeft = switchLeft; + mSwitchTop = switchTop; + mSwitchBottom = switchBottom; + mSwitchRight = switchRight; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the switch + int switchLeft = mSwitchLeft; + int switchTop = mSwitchTop; + int switchRight = mSwitchRight; + int switchBottom = mSwitchBottom; + + mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); + mTrackDrawable.draw(canvas); + + canvas.save(); + + mTrackDrawable.getPadding(mTempRect); + int switchInnerLeft = switchLeft + mTempRect.left; + int switchInnerTop = switchTop + mTempRect.top; + int switchInnerRight = switchRight - mTempRect.right; + int switchInnerBottom = switchBottom - mTempRect.bottom; + canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); + + mThumbDrawable.getPadding(mTempRect); + final int thumbPos = (int) (mThumbPosition + 0.5f); + int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; + int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; + + mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); + mThumbDrawable.draw(canvas); + + // mTextColors should not be null, but just in case + if (mTextColors != null) { + mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(), + mTextColors.getDefaultColor())); + } + mTextPaint.drawableState = getDrawableState(); + + Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; + + canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2, + (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2); + switchText.draw(canvas); + + canvas.restore(); + } + + @Override + public int getCompoundPaddingRight() { + int padding = super.getCompoundPaddingRight() + mSwitchWidth; + if (!TextUtils.isEmpty(getText())) { + padding += mSwitchPadding; + } + return padding; + } + + private int getThumbScrollRange() { + if (mTrackDrawable == null) { + return 0; + } + mTrackDrawable.getPadding(mTempRect); + return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + int[] myDrawableState = getDrawableState(); + + // Set the state of the Drawable + // Drawable may be null when checked state is set from XML, from super constructor + if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState); + if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState); + + invalidate(); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + mThumbDrawable.jumpToCurrentState(); + mTrackDrawable.jumpToCurrentState(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Switch.class.getName()); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Switch.class.getName()); + CharSequence switchText = isChecked() ? mTextOn : mTextOff; + if (!TextUtils.isEmpty(switchText)) { + CharSequence oldText = info.getText(); + if (TextUtils.isEmpty(oldText)) { + info.setText(switchText); + } else { + StringBuilder newText = new StringBuilder(); + newText.append(oldText).append(' ').append(switchText); + info.setText(newText); + } + } + } +} diff --git a/src/com/android/camera/ui/TimeIntervalPopup.java b/src/com/android/camera/ui/TimeIntervalPopup.java new file mode 100644 index 000000000..b79663be2 --- /dev/null +++ b/src/com/android/camera/ui/TimeIntervalPopup.java @@ -0,0 +1,164 @@ +/* + * 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.content.res.Resources; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.NumberPicker; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.camera.R; + +/** + * This is a popup window that allows users to turn on/off time lapse feature, + * and to select a time interval for taking a time lapse video. + */ +public class TimeIntervalPopup extends AbstractSettingPopup { + private static final String TAG = "TimeIntervalPopup"; + private NumberPicker mNumberSpinner; + private NumberPicker mUnitSpinner; + private Switch mTimeLapseSwitch; + private final String[] mUnits; + private final String[] mDurations; + private IconListPreference mPreference; + private Listener mListener; + private Button mConfirmButton; + private TextView mHelpText; + private View mTimePicker; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public TimeIntervalPopup(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = context.getResources(); + mUnits = res.getStringArray(R.array.pref_video_time_lapse_frame_interval_units); + mDurations = res + .getStringArray(R.array.pref_video_time_lapse_frame_interval_duration_values); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Duration + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + + // Units for duration (i.e. seconds, minutes, etc) + mUnitSpinner = (NumberPicker) findViewById(R.id.duration_unit); + mUnitSpinner.setMinValue(0); + mUnitSpinner.setMaxValue(mUnits.length - 1); + mUnitSpinner.setDisplayedValues(mUnits); + mUnitSpinner.setWrapSelectorWheel(false); + + mTimePicker = findViewById(R.id.time_interval_picker); + mTimeLapseSwitch = (Switch) findViewById(R.id.time_lapse_switch); + mHelpText = (TextView) findViewById(R.id.set_time_interval_help_text); + mConfirmButton = (Button) findViewById(R.id.time_lapse_interval_set_button); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + mUnitSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mTimeLapseSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setTimeSelectionEnabled(isChecked); + } + }); + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + } + + private void restoreSetting() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + throw new IllegalArgumentException(); + } else if (index == 0) { + // default choice: time lapse off + mTimeLapseSwitch.setChecked(false); + setTimeSelectionEnabled(false); + } else { + mTimeLapseSwitch.setChecked(true); + setTimeSelectionEnabled(true); + int durationCount = mNumberSpinner.getMaxValue() + 1; + int unit = (index - 1) / durationCount; + int number = (index - 1) % durationCount; + mUnitSpinner.setValue(unit); + mNumberSpinner.setValue(number); + } + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mHelpText.setVisibility(enabled ? GONE : VISIBLE); + mTimePicker.setVisibility(enabled ? VISIBLE : GONE); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + if (mTimeLapseSwitch.isChecked()) { + int newId = mUnitSpinner.getValue() * (mNumberSpinner.getMaxValue() + 1) + + mNumberSpinner.getValue() + 1; + mPreference.setValueIndex(newId); + } else { + mPreference.setValueIndex(0); + } + + if (mListener != null) { + mListener.onListPrefChanged(mPreference); + } + } +} diff --git a/src/com/android/camera/ui/TimerSettingPopup.java b/src/com/android/camera/ui/TimerSettingPopup.java new file mode 100644 index 000000000..06d7e4e50 --- /dev/null +++ b/src/com/android/camera/ui/TimerSettingPopup.java @@ -0,0 +1,153 @@ +/* + * 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 java.util.Locale; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.NumberPicker; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.camera.R; + +/** + * This is a popup window that allows users to turn on/off time lapse feature, + * and to select a time interval for taking a time lapse video. + */ + +public class TimerSettingPopup extends AbstractSettingPopup { + private static final String TAG = "TimerSettingPopup"; + private NumberPicker mNumberSpinner; + private Switch mTimerSwitch; + private String[] mDurations; + private ListPreference mPreference; + private Listener mListener; + private Button mConfirmButton; + private TextView mHelpText; + private View mTimePicker; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public TimerSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(ListPreference preference) { + mPreference = preference; + + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Duration + CharSequence[] entries = mPreference.getEntryValues(); + mDurations = new String[entries.length - 1]; + Locale locale = getResources().getConfiguration().locale; + for (int i = 1; i < entries.length; i++) + mDurations[i-1] = String.format(locale, "%d", + Integer.parseInt(entries[i].toString())); + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + + mTimerSwitch = (Switch) findViewById(R.id.timer_setting_switch); + mHelpText = (TextView) findViewById(R.id.set_timer_help_text); + mConfirmButton = (Button) findViewById(R.id.timer_set_button); + mTimePicker = findViewById(R.id.time_duration_picker); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mTimerSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setTimeSelectionEnabled(isChecked); + } + }); + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + } + + private void restoreSetting() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + throw new IllegalArgumentException(); + } else if (index == 0) { + // default choice: time lapse off + mTimerSwitch.setChecked(false); + setTimeSelectionEnabled(false); + } else { + mTimerSwitch.setChecked(true); + setTimeSelectionEnabled(true); + mNumberSpinner.setValue(index - 1); + } + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mHelpText.setVisibility(enabled ? GONE : VISIBLE); + mTimePicker.setVisibility(enabled ? VISIBLE : GONE); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + if (mTimerSwitch.isChecked()) { + int newId = mNumberSpinner.getValue() + 1; + mPreference.setValueIndex(newId); + } else { + mPreference.setValueIndex(0); + } + + if (mListener != null) { + mListener.onListPrefChanged(mPreference); + } + } +} diff --git a/src/com/android/camera/ui/TwoStateImageView.java b/src/com/android/camera/ui/TwoStateImageView.java new file mode 100644 index 000000000..cd5b27fc1 --- /dev/null +++ b/src/com/android/camera/ui/TwoStateImageView.java @@ -0,0 +1,55 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A @{code ImageView} which change the opacity of the icon if disabled. + */ +public class TwoStateImageView extends ImageView { + private static final int ENABLED_ALPHA = 255; + private static final int DISABLED_ALPHA = (int) (255 * 0.4); + private boolean mFilterEnabled = true; + + public TwoStateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TwoStateImageView(Context context) { + this(context, null); + } + + @SuppressWarnings("deprecation") + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (mFilterEnabled) { + if (enabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public void enableFilter(boolean enabled) { + mFilterEnabled = enabled; + } +} diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java new file mode 100644 index 000000000..10c5e80d4 --- /dev/null +++ b/src/com/android/camera/ui/ZoomRenderer.java @@ -0,0 +1,158 @@ +/* + * 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.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.ScaleGestureDetector; + +import com.android.camera.R; + +public class ZoomRenderer extends OverlayRenderer + implements ScaleGestureDetector.OnScaleGestureListener { + + private static final String TAG = "CAM_Zoom"; + + private int mMaxZoom; + private int mMinZoom; + private OnZoomChangedListener mListener; + + private ScaleGestureDetector mDetector; + private Paint mPaint; + private Paint mTextPaint; + private int mCircleSize; + private int mCenterX; + private int mCenterY; + private float mMaxCircle; + private float mMinCircle; + private int mInnerStroke; + private int mOuterStroke; + private int mZoomSig; + private int mZoomFraction; + private Rect mTextBounds; + + public interface OnZoomChangedListener { + void onZoomStart(); + void onZoomEnd(); + void onZoomValueChanged(int index); // only for immediate zoom + } + + public ZoomRenderer(Context ctx) { + Resources res = ctx.getResources(); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Paint.Style.STROKE); + mTextPaint = new Paint(mPaint); + mTextPaint.setStyle(Paint.Style.FILL); + mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zoom_font_size)); + mTextPaint.setTextAlign(Paint.Align.LEFT); + mTextPaint.setAlpha(192); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mDetector = new ScaleGestureDetector(ctx, this); + mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min); + mTextBounds = new Rect(); + setVisible(false); + } + + // set from module + public void setZoomMax(int zoomMaxIndex) { + mMaxZoom = zoomMaxIndex; + mMinZoom = 0; + } + + public void setZoom(int index) { + mCircleSize = (int) (mMinCircle + index * (mMaxCircle - mMinCircle) / (mMaxZoom - mMinZoom)); + } + + public void setZoomValue(int value) { + value = value / 10; + mZoomSig = value / 10; + mZoomFraction = value % 10; + } + + public void setOnZoomChangeListener(OnZoomChangedListener listener) { + mListener = listener; + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + mMaxCircle = Math.min(getWidth(), getHeight()); + mMaxCircle = (mMaxCircle - mMinCircle) / 2; + } + + public boolean isScaling() { + return mDetector.isInProgress(); + } + + @Override + public void onDraw(Canvas canvas) { + mPaint.setStrokeWidth(mInnerStroke); + canvas.drawCircle(mCenterX, mCenterY, mMinCircle, mPaint); + canvas.drawCircle(mCenterX, mCenterY, mMaxCircle, mPaint); + canvas.drawLine(mCenterX - mMinCircle, mCenterY, + mCenterX - mMaxCircle - 4, mCenterY, mPaint); + mPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mCenterX, (float) mCenterY, + (float) mCircleSize, mPaint); + String txt = mZoomSig+"."+mZoomFraction+"x"; + mTextPaint.getTextBounds(txt, 0, txt.length(), mTextBounds); + canvas.drawText(txt, mCenterX - mTextBounds.centerX(), mCenterY - mTextBounds.centerY(), + mTextPaint); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + final float sf = detector.getScaleFactor(); + float circle = (int) (mCircleSize * sf * sf); + circle = Math.max(mMinCircle, circle); + circle = Math.min(mMaxCircle, circle); + if (mListener != null && (int) circle != mCircleSize) { + mCircleSize = (int) circle; + int zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle)); + mListener.onZoomValueChanged(zoom); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setVisible(true); + if (mListener != null) { + mListener.onZoomStart(); + } + update(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + setVisible(false); + if (mListener != null) { + mListener.onZoomEnd(); + } + } + +} -- cgit v1.2.3