diff options
author | Doris Liu <tianliu@google.com> | 2013-03-14 21:01:35 -0700 |
---|---|---|
committer | Doris Liu <tianliu@google.com> | 2013-04-09 10:35:50 -0700 |
commit | 753bb8aa56fff425fe16b93be368b9b236e4751f (patch) | |
tree | 92182a98004dba4fb131e9fcb020796625659276 /src | |
parent | 15f4efb50b40fbdf108121e367bfd6f50d5b2c41 (diff) | |
download | android_packages_apps_Snap-753bb8aa56fff425fe16b93be368b9b236e4751f.tar.gz android_packages_apps_Snap-753bb8aa56fff425fe16b93be368b9b236e4751f.tar.bz2 android_packages_apps_Snap-753bb8aa56fff425fe16b93be368b9b236e4751f.zip |
Work in progress - Put preview in TextureView
Fixed gesture recognizing and pie menu.
Fixed camera picker. Exposure setting got fixed with rebasing.
Rebased for new pie menu
Fixed camera mode switch listener
Ongoing: secure album, aspect ratio for video recording on ICS
Change-Id: Iedae80815faf81cb49c791885810c8427034a6d1
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/camera/NewCameraActivity.java | 326 | ||||
-rw-r--r-- | src/com/android/camera/NewCameraModule.java | 76 | ||||
-rw-r--r-- | src/com/android/camera/NewPhotoMenu.java | 247 | ||||
-rw-r--r-- | src/com/android/camera/NewPhotoModule.java | 2005 | ||||
-rw-r--r-- | src/com/android/camera/NewPhotoUI.java | 879 | ||||
-rw-r--r-- | src/com/android/camera/NewPreviewGestures.java | 358 | ||||
-rw-r--r-- | src/com/android/camera/NewVideoMenu.java | 190 | ||||
-rw-r--r-- | src/com/android/camera/NewVideoModule.java | 2341 | ||||
-rw-r--r-- | src/com/android/camera/NewVideoUI.java | 716 | ||||
-rw-r--r-- | src/com/android/camera/PhotoMenu.java | 2 | ||||
-rw-r--r-- | src/com/android/camera/PieController.java | 8 | ||||
-rw-r--r-- | src/com/android/camera/VideoMenu.java | 2 | ||||
-rw-r--r-- | src/com/android/camera/VideoUI.java | 1 | ||||
-rw-r--r-- | src/com/android/camera/ui/CameraSwitcher.java | 33 | ||||
-rw-r--r-- | src/com/android/camera/ui/FaceView.java | 26 | ||||
-rw-r--r-- | src/com/android/camera/ui/NewCameraRootView.java | 92 |
16 files changed, 7295 insertions, 7 deletions
diff --git a/src/com/android/camera/NewCameraActivity.java b/src/com/android/camera/NewCameraActivity.java new file mode 100644 index 000000000..46295d8f2 --- /dev/null +++ b/src/com/android/camera/NewCameraActivity.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.IBinder; +import android.provider.Settings; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.NewCameraRootView; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; + +public class NewCameraActivity extends Activity + implements 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; + 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 mCurrentModuleIndex; + private NewCameraModule mCurrentModule; + private View mRootView; + private int mResultCodeForTesting; + private Intent mResultDataForTesting; + private OnScreenHint mStorageHint; + private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD; + private PhotoModule mController; + private boolean mAutoRotateScreen; + private boolean mSecureCamera; + private int mLastRawOrientation; + private MyOrientationEventListener mOrientationListener; + 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); + } + } + private MediaSaveService mMediaSaveService; + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder b) { + mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService(); + mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + } + @Override + public void onServiceDisconnected(ComponentName className) { + mMediaSaveService = null; + }}; + + public MediaSaveService getMediaSaveService() { + return mMediaSaveService; + } + + private void bindMediaSaveService() { + Intent intent = new Intent(this, MediaSaveService.class); + startService(intent); // start service before binding it so the + // service won't be killed if we unbind it. + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + private void unbindMediaSaveService() { + mMediaSaveService.setListener(null); + unbindService(mConnection); + } + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + setContentView(R.layout.camera); + if (ApiHelper.HAS_ROTATION_ANIMATION) { + setRotationAnimation(); + } + // 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. + //TODO: sSecureAlbumId++; + } else if (ACTION_IMAGE_CAPTURE_SECURE.equals(action)) { + mSecureCamera = true; + } else { + mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false); + } + /*TODO: if (mSecureCamera) { + IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenOffReceiver, filter); + if (sScreenOffReceiver == null) { + sScreenOffReceiver = new ScreenOffReceiver(); + getApplicationContext().registerReceiver(sScreenOffReceiver, filter); + } + }*/ + mRootView = findViewById(R.id.camera_app_root); + mCurrentModule = new NewPhotoModule(); + mCurrentModule.init(this, mRootView); + mOrientationListener = new MyOrientationEventListener(this); + bindMediaSaveService(); + } + + private void setRotationAnimation() { + int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; + rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; + Window win = getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + winParams.rotationAnimation = rotationAnimation; + win.setAttributes(winParams); + } + + @Override + public void onUserInteraction() { + super.onUserInteraction(); + mCurrentModule.onUserInteraction(); + } + + @Override + public void onPause() { + mOrientationListener.disable(); + mCurrentModule.onPauseBeforeSuper(); + super.onPause(); + mCurrentModule.onPauseAfterSuper(); + } + + @Override + public void onResume() { + if (Settings.System.getInt(getContentResolver(), + Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + mAutoRotateScreen = false; + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + mAutoRotateScreen = true; + } + mOrientationListener.enable(); + mCurrentModule.onResumeBeforeSuper(); + super.onResume(); + mCurrentModule.onResumeAfterSuper(); + } + + @Override + public void onDestroy() { + unbindMediaSaveService(); + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + mCurrentModule.onConfigurationChanged(config); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + return mCurrentModule.dispatchTouchEvent(m); + } + public boolean isAutoRotateScreen() { + return mAutoRotateScreen; + } + + 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 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; + } + + public boolean isSecureCamera() { + return mSecureCamera; + } + + @Override + public void onCameraSelected(int i) { + if (mCurrentModuleIndex == i) return; + + CameraHolder.instance().keep(); + closeModule(mCurrentModule); + mCurrentModuleIndex = i; + switch (i) { + case VIDEO_MODULE_INDEX: + mCurrentModule = new NewVideoModule(); + break; + case PHOTO_MODULE_INDEX: + mCurrentModule = new NewPhotoModule(); + break; + /* TODO: + case PANORAMA_MODULE_INDEX: + mCurrentModule = new PanoramaModule(); + break; + case LIGHTCYCLE_MODULE_INDEX: + mCurrentModule = LightCycleHelper.createPanoramaModule(); + break; */ + default: + break; + } + + openModule(mCurrentModule); + mCurrentModule.onOrientationChanged(mLastRawOrientation); + if (mMediaSaveService != null) { + mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + } + } + + private void openModule(NewCameraModule module) { + module.init(this, mRootView); + module.onResumeBeforeSuper(); + module.onResumeAfterSuper(); + ((NewCameraRootView) mRootView).cameraModuleChanged(); + } + + private void closeModule(NewCameraModule module) { + module.onPauseBeforeSuper(); + module.onPauseAfterSuper(); + ((ViewGroup) mRootView).removeAllViews(); + } + + @Override + public void onShowSwitcherPopup() { + } +}
\ No newline at end of file diff --git a/src/com/android/camera/NewCameraModule.java b/src/com/android/camera/NewCameraModule.java new file mode 100644 index 000000000..061cc6cca --- /dev/null +++ b/src/com/android/camera/NewCameraModule.java @@ -0,0 +1,76 @@ +/* + * 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.Intent; +import android.content.res.Configuration; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public interface NewCameraModule { + + public void init(NewCameraActivity activity, View frame); + + 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 needsSwitcher(); + + public boolean needsPieMenu(); + + public void onOrientationChanged(int orientation); + + public void onShowSwitcherPopup(); + + public void onMediaSaveServiceConnected(MediaSaveService s); +} diff --git a/src/com/android/camera/NewPhotoMenu.java b/src/com/android/camera/NewPhotoMenu.java new file mode 100644 index 000000000..962df0f9a --- /dev/null +++ b/src/com/android/camera/NewPhotoMenu.java @@ -0,0 +1,247 @@ +/* + * 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.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; +import com.android.gallery3d.R; + +public class NewPhotoMenu extends PieController + implements MoreSettingPopup.Listener, + TimerSettingPopup.Listener, + ListPrefSettingPopup.Listener { + private static String TAG = "CAM_photomenu"; + + private static final int POS_HDR = 0; + private static final int POS_EXP = 1; + private static final int POS_MORE = 2; + private static final int POS_FLASH = 3; + private static final int POS_SWITCH = 4; + private static final int POS_WB = 1; + private static final int POS_SET = 2; + + private final String mSettingOff; + + private NewPhotoUI mUI; + private String[] mOtherKeys; + // First level popup + private MoreSettingPopup mPopup; + // Second level popup + private AbstractSettingPopup mSecondPopup; + private NewCameraActivity mActivity; + + public NewPhotoMenu(NewCameraActivity activity, NewPhotoUI ui, PieRenderer pie) { + super(activity, pie); + mUI = ui; + mSettingOff = activity.getString(R.string.setting_off_value); + mActivity = activity; + } + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + mSecondPopup = null; + PieItem item = null; + // flash + if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) { + item = makeItem(CameraSettings.KEY_FLASH_MODE, POS_FLASH, 5); + mRenderer.addItem(item); + } + // exposure compensation + item = makeItem(CameraSettings.KEY_EXPOSURE, POS_EXP, 5); + mRenderer.addItem(item); + // camera switcher + if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) { + item = makeItem(R.drawable.ic_switch_photo_facing_holo_light); + item.setPosition(POS_SWITCH, 5); + 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); + } + // hdr + if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) { + item = makeItem(R.drawable.ic_hdr); + item.setPosition(POS_HDR, 5); + item.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(item); + } + + // more settings + PieItem more = makeItem(R.drawable.ic_settings_holo_light); + more.setPosition(POS_MORE, 5); + mRenderer.addItem(more); + // white balance + item = makeItem(CameraSettings.KEY_WHITE_BALANCE, POS_WB, 5); + more.addItem(item); + // settings popup + 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, + }; + item = makeItem(R.drawable.ic_settings_holo_light); + item.setPosition(POS_SET, 5); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + if (mPopup == null) { + initializePopup(); + } + mUI.showPopup(mPopup); + } + }); + more.addItem(item); + } + + @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) { + mUI.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) mUI.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); + mUI.dismissPopup(true); + mSecondPopup = timerPopup; + } else { + ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate( + R.layout.list_pref_setting_popup, null, false); + basic.initialize(pref); + basic.setSettingChangedListener(this); + mUI.dismissPopup(true); + mSecondPopup = basic; + } + mUI.showPopup(mSecondPopup); + } +} diff --git a/src/com/android/camera/NewPhotoModule.java b/src/com/android/camera/NewPhotoModule.java new file mode 100644 index 000000000..dfa1e0cc4 --- /dev/null +++ b/src/com/android/camera/NewPhotoModule.java @@ -0,0 +1,2005 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +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.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.Size; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +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.KeyEvent; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.WindowManager; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.CountDownView.OnCountDownFinishedListener; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.RotateTextToast; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.Rational; +import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.util.UsageStatistics; + +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 NewPhotoModule + implements NewCameraModule, + PhotoController, + FocusOverlayManager.Listener, + CameraPreference.OnPreferenceChangedListener, + ShutterButton.OnShutterButtonListener, + MediaSaveService.Listener, + OnCountDownFinishedListener, + SensorEventListener { + + 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; + + // 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 NewCameraActivity mActivity; + private CameraProxy mCameraDevice; + private int mCameraId; + private Parameters mParameters; + private boolean mPaused; + + private NewPhotoUI mUI; + + // -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 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 boolean mFaceDetectionStarted = false; + + // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true. + private String mCropValue; + private Uri mSaveUri; + + // We use a queue to generated names of the images to be used later + // when the image is ready to be saved. + 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 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 String mSceneMode; + + private final Handler mHandler = new MainHandler(); + private PreferenceGroup mPreferenceGroup; + + private boolean mQuickCapture; + private SensorManager mSensorManager; + private float[] mGData = new float[3]; + private float[] mMData = new float[3]; + private float[] mR = new float[16]; + private int mHeading = -1; + + CameraStartUpThread mCameraStartUpThread; + ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable(); + + private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + // TODO: Commenting out the line below for now. need to get it working + // mActivity.addSecureAlbumItemIfNeeded(false, uri); + 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: { + // TODO: Need to revisit + // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + break; + } + + case CAMERA_OPEN_DONE: { + onCameraOpened(); + break; + } + + case START_PREVIEW_DONE: { + onPreviewStarted(); + 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; + } + } + } + } + + @Override + public void init(NewCameraActivity activity, View parent) { + mActivity = activity; + mUI = new NewPhotoUI(activity, this, 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(); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsImageCaptureIntent = isImageCaptureIntent(); + + 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); + mLocationManager = new LocationManager(mActivity, mUI); + mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE)); + } + + private void initializeControlByIntent() { + mUI.initializeControlByIntent(); + if (mIsImageCaptureIntent) { + setupCaptureParams(); + } + } + + private void onPreviewStarted() { + mCameraStartUpThread = null; + setCameraState(IDLE); + startFaceDetection(); + locationFirstRun(); + } + + // 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 onCameraOpened() { + View root = mUI.getRootView(); + // These depend on camera parameters. + + int width = root.getWidth(); + int height = root.getHeight(); + mFocusManager.setPreviewSize(width, height); + openCameraCommon(); + } + + private void switchCamera() { + if (mPaused) return; + + Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + setCameraId(mCameraId); + + // from onPause + closeCamera(); + mUI.collapseCameraControls(); + mUI.clearFaces(); + 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(); + + openCameraCommon(); + + 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); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + // either open a new camera or switch cameras + private void openCameraCommon() { + loadCameraPreferences(); + + mUI.onCameraOpened(mPreferenceGroup, mPreferences, mParameters, this); + updateSceneMode(); + showTapToFocusToastIfNeeded(); + + + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + if (mFocusManager != null) mFocusManager.setPreviewSize(width, height); + } + + 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(); + + mUI.initializeFirstTime(); + MediaSaveService s = mActivity.getMediaSaveService(); + // We set the listener only when both service and shutterbutton + // are initialized. + if (s != null) { + s.setListener(this); + } + + mNamedImages = new NamedImages(); + + mFirstTimeInitialized = true; + addIdleHandler(); + + mActivity.updateStorageSpaceAndHint(); + } + + // 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); + MediaSaveService s = mActivity.getMediaSaveService(); + if (s != null) { + s.setListener(this); + } + mNamedImages = new NamedImages(); + mUI.initializeSecondTime(mParameters); + keepMediaProviderInstance(); + } + + @Override + public void onSurfaceCreated(SurfaceHolder 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(); + } + } + + 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; + } + }); + } + + @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; + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + mUI.onStartFaceDetection(mDisplayOrientation, + (info.facing == CameraInfo.CAMERA_FACING_FRONT)); + mCameraDevice.setFaceDetectionListener(mUI); + 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(); + mUI.clearFaces(); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mCameraState == SWITCHING_CAMERA) return true; + return mUI.dispatchTouchEvent(m); + } + + 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) { + mUI.showSwitcher(); + //TODO: 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"); + + /*TODO: + // 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(); + ExifInterface exif = Exif.getExif(jpegData); + int orientation = Exif.getOrientation(exif); + 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; + if (mHeading >= 0) { + // heading direction has been updated by the sensor. + ExifTag directionRefTag = exif.buildTag( + ExifInterface.TAG_GPS_IMG_DIRECTION_REF, + ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION); + ExifTag directionTag = exif.buildTag( + ExifInterface.TAG_GPS_IMG_DIRECTION, + new Rational(mHeading, 1)); + exif.setTag(directionRefTag); + exif.setTag(directionTag); + } + mActivity.getMediaSaveService().addImage( + jpegData, title, date, mLocation, width, height, + orientation, exif, mOnMediaSavedListener, mContentResolver); + } + } else { + mJpegImageData = jpegData; + if (!mQuickCapture) { + mUI.showPostCaptureAlert(); + } else { + onCaptureDone(); + } + } + + // 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, mUI.isShutterPressed()); + } + } + + @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<NamedEntity> mQueue; + private boolean mStop; + private NamedEntity mNamedEntity; + + public NamedImages() { + mQueue = new ArrayList<NamedEntity>(); + } + + 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 PhotoController.PREVIEW_STOPPED: + case PhotoController.SNAPSHOT_IN_PROGRESS: + case PhotoController.FOCUSING: + case PhotoController.SWITCHING_CAMERA: + mUI.enableGestures(false); + break; + case PhotoController.IDLE: + mUI.enableGestures(true); + break; + } + } + + private void animateFlash() { + /* //TODO: + // 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 + || mActivity.getMediaSaveService().isQueueFull()) { + 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. + int orientation = (360 - mDisplayRotation) % 360; + // We need to be consistent with the framework orientation (i.e. the + // orientation of the UI.) when the auto-rotate screen setting is on. + if (mActivity.isAutoRotateScreen()) { + orientation = (360 - mDisplayRotation) % 360; + } else { + orientation = mOrientation; + } + mJpegRotation = Util.getJpegRotation(mCameraId, orientation); + 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 updateSceneMode() { + // 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) { + mUI.overrideSettings( + CameraSettings.KEY_FLASH_MODE, flashMode, + CameraSettings.KEY_WHITE_BALANCE, whiteBalance, + CameraSettings.KEY_FOCUS_MODE, focusMode); + } + + private void loadCameraPreferences() { + CameraSettings settings = new CameraSettings(mActivity, mInitialParams, + mCameraId, CameraHolder.instance().getCameraInfo()); + mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences); + } + + @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; + } + } + + @Override + public void onCaptureCancelled() { + mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent()); + mActivity.finish(); + } + + @Override + public void onCaptureRetake() { + if (mPaused) + return; + mUI.hidePostCaptureAlert(); + setupPreview(); + } + + @Override + public void onCaptureDone() { + 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 { + ExifInterface exif = Exif.getExif(data); + int orientation = Exif.getOrientation(exif); + 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); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + if (mPaused || mUI.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) { + mUI.hideSwitcher(); + //TODO: mActivity.setSwipingEnabled(false); + } + mFocusManager.onShutterDown(); + } else { + // for countdown mode, we need to postpone the shutter release + // i.e. lock the focus during countdown. + if (!mUI.isCountingDown()) { + mFocusManager.onShutterUp(); + } + } + } + + @Override + public void onShutterButtonClick() { + if (mPaused || mUI.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 (mUI.isCountingDown()) { + mUI.cancelCountDown(); + } + if (seconds > 0) { + mUI.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); + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "PhotoModule"); + + Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (gsensor != null) { + mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL); + } + + Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + if (msensor != null) { + mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL); + } + } + + void waitCameraStartUpThread() { + try { + if (mCameraStartUpThread != null) { + mCameraStartUpThread.cancel(); + mCameraStartUpThread.join(); + mCameraStartUpThread = null; + setCameraState(IDLE); + } + } catch (InterruptedException e) { + // ignore + } + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (gsensor != null) { + mSensorManager.unregisterListener(this, gsensor); + } + + Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + if (msensor != null) { + mSensorManager.unregisterListener(this, msensor); + } + } + + @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(); + + 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); + + closeCamera(); + + resetScreenOn(); + mUI.onPause(); + + mPendingSwitchCameraId = -1; + if (mFocusManager != null) mFocusManager.removeMessages(); + MediaSaveService s = mActivity.getMediaSaveService(); + if (s != null) { + s.setListener(null); + } + } + + /** + * 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. + // 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(), mUI); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged"); + setDisplayOrientation(); + } + + @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 (mUI.removeTopLevelPopup()) return; + + // Check if metering area or focus area is supported. + if (!mFocusAreaSupported && !mMeteringAreaSupported) return; + mFocusManager.onSingleTapUp(x, y); + } + + @Override + public boolean onBackPressed() { + return mUI.onBackPressed(); + } + + @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 (/*TODO: 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 (mUI.removeTopLevelPopup()) return true; + onShutterButtonFocus(true); + mUI.pressShutterButton(); + } + 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 = mDisplayOrientation; + mUI.setDisplayOrientation(mDisplayOrientation); + if (mFocusManager != null) { + mFocusManager.setDisplayOrientation(mDisplayOrientation); + } + // Change the camera display orientation + if (mCameraDevice != null) { + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + } + } + + // 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); + // Let UI set its expected aspect ratio + mUI.setPreviewSize(mParameters.getPreviewSize()); + Object st = mUI.getSurfaceTexture(); + if (st != null) { + mCameraDevice.setPreviewTextureAsync((SurfaceTexture) st); + } + + Log.v(TAG, "startPreview"); + mCameraDevice.startPreviewAsync(); + mFocusManager.onPreviewStarted(); + + if (mSnapshotOnIdle) { + mHandler.post(mDoSnapRunnable); + } + } + + @Override + public 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<Integer> 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<Size> 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<Size> 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<String> 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); + updateSceneMode(); + mUpdateSet = 0; + } else { + if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) { + mHandler.sendEmptyMessageDelayed( + SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000); + } + } + } + + public boolean isCameraIdle() { + return (mCameraState == IDLE) || + (mCameraState == PREVIEW_STOPPED) || + ((mFocusManager != null) && mFocusManager.isFocusCompleted() + && (mCameraState != SWITCHING_CAMERA)); + } + + public 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"); + } + } + + @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); + mUI.updateOnScreenIndicators(mParameters, mPreferences); + } + + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + + Log.v(TAG, "Start to switch camera. cameraId=" + cameraId); + // We need to keep a preview frame for the animation before + // releasing the camera. This will trigger onPreviewTextureCopied. + //TODO: Need to animate the camera switch + switchCamera(); + } + + // 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); + } + + @Override + public void onOverriddenPreferencesClicked() { + if (mPaused) return; + mUI.showPreferencesToast(); + } + + 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); + } + + @Override + public void onCountDownFinished() { + mSnapshotOnIdle = false; + mFocusManager.doSnap(); + mFocusManager.onShutterUp(); + } + + @Override + public boolean needsSwitcher() { + return !mIsImageCaptureIntent; + } + + @Override + public boolean needsPieMenu() { + return true; + } + + @Override + public void onShowSwitcherPopup() { + mUI.onShowSwitcherPopup(); + } + + @Override + public int onZoomChanged(int index) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return index; + mZoomValue = index; + if (mParameters == null || mCameraDevice == null) return index; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mCameraDevice.setParametersAsync(mParameters); + Parameters p = mCameraDevice.getParameters(); + if (p != null) return p.getZoom(); + return index; + } + + @Override + public int getCameraState() { + return mCameraState; + } + + @Override + public void onQueueStatus(boolean full) { + mUI.enableShutter(!full); + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // We set the listener only when both service and shutterbutton + // are initialized. + if (mFirstTimeInitialized) { + s.setListener(this); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + int type = event.sensor.getType(); + float[] data; + if (type == Sensor.TYPE_ACCELEROMETER) { + data = mGData; + } else if (type == Sensor.TYPE_MAGNETIC_FIELD) { + data = mMData; + } else { + // we should not be here. + return; + } + for (int i = 0; i < 3 ; i++) { + data[i] = event.values[i]; + } + float[] orientation = new float[3]; + SensorManager.getRotationMatrix(mR, null, mGData, mMData); + SensorManager.getOrientation(mR, orientation); + mHeading = (int) (orientation[0] * 180f / Math.PI) % 360; + if (mHeading < 0) { + mHeading += 360; + } + } +/* Below is no longer needed, except to get rid of compile error + * TODO: Remove these + */ + + // TODO: Delete this function after old camera code is removed + @Override + public void onRestorePreferencesClicked() {} + + @Override + public void onFullScreenChanged(boolean full) { + /* //TODO: + mUI.onFullScreenChanged(full); + if (ApiHelper.HAS_SURFACE_TEXTURE) { + if (mActivity.mCameraScreenNail != null) { + ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full); + } + return; + } */ + } + +} diff --git a/src/com/android/camera/NewPhotoUI.java b/src/com/android/camera/NewPhotoUI.java new file mode 100644 index 000000000..97f929288 --- /dev/null +++ b/src/com/android/camera/NewPhotoUI.java @@ -0,0 +1,879 @@ +/* + * 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.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.Face; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.FocusOverlayManager.FocusUI; +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.CountDownView; +import com.android.camera.ui.CountDownView.OnCountDownFinishedListener; +import com.android.camera.ui.CameraSwitcher; +import com.android.camera.ui.FaceView; +import com.android.camera.ui.FocusIndicator; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.PieRenderer.PieListener; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.io.IOException; +import java.util.List; + +public class NewPhotoUI implements PieListener, + NewPreviewGestures.SingleTapListener, + NewPreviewGestures.CancelEventListener, + FocusUI, TextureView.SurfaceTextureListener, + LocationManager.Listener, + FaceDetectionListener, + NewPreviewGestures.SwipeListener { + + private static final String TAG = "CAM_UI"; + private static final int UPDATE_TRANSFORM_MATRIX = 1; + private NewCameraActivity mActivity; + private PhotoController mController; + private NewPreviewGestures mGestures; + + private View mRootView; + private Object mSurfaceTexture; + + private AbstractSettingPopup mPopup; + private ShutterButton mShutterButton; + private CountDownView mCountDownView; + + private FaceView mFaceView; + private RenderOverlay mRenderOverlay; + private View mReviewCancelButton; + private View mReviewDoneButton; + private View mReviewRetakeButton; + + private View mMenuButton; + private View mBlocker; + private NewPhotoMenu mMenu; + private CameraSwitcher mSwitcher; + private View mCameraControls; + + // 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; + + private PieRenderer mPieRenderer; + private ZoomRenderer mZoomRenderer; + private Toast mNotSelectableToast; + + private int mZoomMax; + private List<Integer> mZoomRatios; + + private int mPreviewWidth = 0; + private int mPreviewHeight = 0; + private float mSurfaceTextureUncroppedWidth; + private float mSurfaceTextureUncroppedHeight; + + private SurfaceTextureSizeChangedListener mSurfaceTextureSizeListener; + private TextureView mTextureView; + private Matrix mMatrix = null; + private float mAspectRatio = 4f / 3f; + private final Object mLock = new Object(); + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_TRANSFORM_MATRIX: + setTransformMatrix(mPreviewWidth, mPreviewHeight); + break; + default: + break; + } + } + }; + + public interface SurfaceTextureSizeChangedListener { + public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight); + } + + private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + int width = right - left; + int height = bottom - top; + // Full-screen screennail + int w = width; + int h = height; + if (Util.getDisplayRotation(mActivity) % 180 != 0) { + w = height; + h = width; + } + if (mPreviewWidth != width || mPreviewHeight != height) { + mPreviewWidth = width; + mPreviewHeight = height; + onScreenSizeChanged(width, height, w, h); + mController.onScreenSizeChanged(width, height, w, h); + } + } + }; + + public NewPhotoUI(NewCameraActivity activity, PhotoController controller, View parent) { + mActivity = activity; + mController = controller; + mRootView = parent; + + mActivity.getLayoutInflater().inflate(R.layout.new_photo_module, + (ViewGroup) mRootView, true); + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + // display the view + mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content); + mTextureView.setSurfaceTextureListener(this); + mTextureView.addOnLayoutChangeListener(mLayoutListener); + initIndicators(); + + mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); + mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(0); + mSwitcher.setSwitchListener((CameraSwitchListener) mActivity); + mMenuButton = mRootView.findViewById(R.id.menu); + if (ApiHelper.HAS_FACE_DETECTION) { + ViewStub faceViewStub = (ViewStub) mRootView + .findViewById(R.id.face_view_stub); + if (faceViewStub != null) { + faceViewStub.inflate(); + mFaceView = (FaceView) mRootView.findViewById(R.id.face_view); + setSurfaceTextureSizeChangedListener( + (SurfaceTextureSizeChangedListener) mFaceView); + } + } + mCameraControls = mRootView.findViewById(R.id.camera_controls); + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + setTransformMatrix(width, height); + } + + public void setSurfaceTextureSizeChangedListener(SurfaceTextureSizeChangedListener listener) { + mSurfaceTextureSizeListener = listener; + } + + public void setPreviewSize(Size size) { + int width = size.width; + int height = size.height; + if (width == 0 || height == 0) { + Log.w(TAG, "Preview size should not be 0."); + return; + } + if (width > height) { + mAspectRatio = (float) width / height; + } else { + mAspectRatio = (float) height / width; + } + mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX); + } + + private void setTransformMatrix(int width, int height) { + mMatrix = mTextureView.getTransform(mMatrix); + int orientation = Util.getDisplayRotation(mActivity); + float scaleX = 1f, scaleY = 1f; + float scaledTextureWidth, scaledTextureHeight; + if (width > height) { + scaledTextureWidth = Math.max(width, + (int) (height * mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int)(width / mAspectRatio)); + } else { + scaledTextureWidth = Math.max(width, + (int) (height / mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int) (width * mAspectRatio)); + } + + if (mSurfaceTextureUncroppedWidth != scaledTextureWidth || + mSurfaceTextureUncroppedHeight != scaledTextureHeight) { + mSurfaceTextureUncroppedWidth = scaledTextureWidth; + mSurfaceTextureUncroppedHeight = scaledTextureHeight; + if (mSurfaceTextureSizeListener != null) { + mSurfaceTextureSizeListener.onSurfaceTextureSizeChanged( + (int) mSurfaceTextureUncroppedWidth, (int) mSurfaceTextureUncroppedHeight); + } + } + scaleX = scaledTextureWidth / width; + scaleY = scaledTextureHeight / height; + mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2); + mTextureView.setTransform(mMatrix); + } + + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + synchronized (mLock) { + mSurfaceTexture = surface; + mLock.notifyAll(); + } + } + + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + // Ignored, Camera does all the work for us + } + + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mSurfaceTexture = null; + mController.stopPreview(); + Log.w(TAG, "surfaceTexture is destroyed"); + return true; + } + + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // Invoked every time there's a new Camera preview frame + } + + public View getRootView() { + return mRootView; + } + + private void initIndicators() { + mOnScreenIndicators = mActivity.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); + } + + public void onCameraOpened(PreferenceGroup prefGroup, ComboPreferences prefs, + Camera.Parameters params, OnPreferenceChangedListener listener) { + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mPieRenderer.setPieListener(this); + } + + if (mMenu == null) { + mMenu = new NewPhotoMenu(mActivity, this, mPieRenderer); + mMenu.setListener(listener); + } + mMenu.initialize(prefGroup); + + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + } + mRenderOverlay.addRenderer(mPieRenderer); + mRenderOverlay.addRenderer(mZoomRenderer); + + if (mGestures == null) { + // this will handle gesture disambiguation and dispatching + mGestures = new NewPreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer, + this); + mGestures.setCancelEventListener(this); + } + mGestures.clearTouchReceivers(); + mGestures.setRenderOverlay(mRenderOverlay); + mGestures.addTouchReceiver(mMenuButton); + mGestures.addTouchReceiver(mBlocker); + // make sure to add touch targets for image capture + if (mController.isImageCaptureIntent()) { + if (mReviewCancelButton != null) { + mGestures.addTouchReceiver(mReviewCancelButton); + } + if (mReviewDoneButton != null) { + mGestures.addTouchReceiver(mReviewDoneButton); + } + } + mRenderOverlay.requestLayout(); + + initializeZoom(params); + updateOnScreenIndicators(params, prefs); + } + + private void openMenu() { + if (mPieRenderer != null) { + // If autofocus is not finished, cancel autofocus so that the + // subsequent touch can be handled by PreviewGestures + if (mController.getCameraState() == PhotoController.FOCUSING) { + mController.cancelAutoFocus(); + } + mPieRenderer.showInCenter(); + } + } + + public void initializeControlByIntent() { + mBlocker = mActivity.findViewById(R.id.blocker); + mMenuButton = mActivity.findViewById(R.id.menu); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openMenu(); + } + }); + if (mController.isImageCaptureIntent()) { + hideSwitcher(); + ViewGroup cameraControls = (ViewGroup) mActivity.findViewById(R.id.camera_controls); + mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls); + + mReviewDoneButton = mActivity.findViewById(R.id.btn_done); + mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel); + mReviewRetakeButton = mActivity.findViewById(R.id.btn_retake); + mReviewCancelButton.setVisibility(View.VISIBLE); + + mReviewDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureDone(); + } + }); + mReviewCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureCancelled(); + } + }); + + mReviewRetakeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureRetake(); + } + }); + } + } + + public void hideUI() { + mCameraControls.setVisibility(View.INVISIBLE); + hideSwitcher(); + mShutterButton.setVisibility(View.GONE); + } + + public void showUI() { + mCameraControls.setVisibility(View.VISIBLE); + showSwitcher(); + mShutterButton.setVisibility(View.VISIBLE); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void showSwitcher() { + mSwitcher.setVisibility(View.VISIBLE); + } + + // called from onResume but only the first time + public void initializeFirstTime() { + // Initialize shutter button. + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(mController); + mShutterButton.setVisibility(View.VISIBLE); + } + + // called from onResume every other time + public void initializeSecondTime(Camera.Parameters params) { + initializeZoom(params); + if (mController.isImageCaptureIntent()) { + hidePostCaptureAlert(); + } + if (mMenu != null) { + mMenu.reloadPreferences(); + } + } + + public void initializeZoom(Camera.Parameters params) { + if ((params == null) || !params.isZoomSupported() + || (mZoomRenderer == null)) return; + mZoomMax = params.getMaxZoom(); + mZoomRatios = params.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(params.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(params.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + } + + public void showGpsOnScreenIndicator(boolean hasSignal) { } + + public void hideGpsOnScreenIndicator() { } + + public void overrideSettings(final String ... keyvalues) { + mMenu.overrideSettings(keyvalues); + } + + public void updateOnScreenIndicators(Camera.Parameters params, + ComboPreferences prefs) { + if (params == null) return; + updateSceneOnScreenIndicator(params.getSceneMode()); + updateExposureOnScreenIndicator(params, + CameraSettings.readExposure(prefs)); + updateFlashOnScreenIndicator(params.getFlashMode()); + updateHdrOnScreenIndicator(params.getSceneMode()); + } + + private void updateExposureOnScreenIndicator(Camera.Parameters params, int value) { + if (mExposureIndicator == null) { + return; + } + int id = 0; + float step = params.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); + } + } + + public void setCameraState(int state) { + } + + // Gestures and touch events + + public boolean dispatchTouchEvent(MotionEvent m) { + if (mPopup != null || mSwitcher.showsPopup()) { + boolean handled = mRootView.dispatchTouchEvent(m); + if (!handled && mPopup != null) { + dismissPopup(false); + } + return handled; + } else if (mGestures != null && mRenderOverlay != null) { + if (mGestures.dispatchTouch(m)) { + return true; + } else { + return mRootView.dispatchTouchEvent(m); + } + } + return true; + } + + @Override + public void onTouchEventCancelled(MotionEvent cancelEvent) { + mRootView.dispatchTouchEvent(cancelEvent); + } + + public void enableGestures(boolean enable) { + if (mGestures != null) { + mGestures.setEnabled(enable); + } + } + + // forward from preview gestures to controller + @Override + public void onSingleTapUp(View view, int x, int y) { + mController.onSingleTapUp(view, x, y); + } + + 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 (mController.isImageCaptureIntent()) { + if (!removeTopLevelPopup()) { + // no popup to dismiss, cancel image capture + mController.onCaptureCancelled(); + } + return true; + } else if (!mController.isCameraIdle()) { + // ignore backs while we're taking a picture + return true; + } else { + return removeTopLevelPopup(); + } + } + + 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(); + } + + 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; + } + + public void showPopup(AbstractSettingPopup popup) { + 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) { + showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mMenu.popupDismissed(topOnly); + } + + public void onShowSwitcherPopup() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenuButton != null) { + mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + public boolean collapseCameraControls() { + // Remove all the popups/dialog boxes + boolean ret = false; + if (mPopup != null) { + dismissPopup(false); + ret = true; + } + return ret; + } + + protected void showPostCaptureAlert() { + mOnScreenIndicators.setVisibility(View.GONE); + mMenuButton.setVisibility(View.GONE); + Util.fadeIn(mReviewDoneButton); + mShutterButton.setVisibility(View.INVISIBLE); + Util.fadeIn(mReviewRetakeButton); + } + + protected void hidePostCaptureAlert() { + mOnScreenIndicators.setVisibility(View.VISIBLE); + mMenuButton.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewDoneButton); + mShutterButton.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewRetakeButton); + } + + public void setDisplayOrientation(int orientation) { + if (mFaceView != null) { + mFaceView.setDisplayOrientation(orientation); + } + } + + // shutter button handling + + public boolean isShutterPressed() { + return mShutterButton.isPressed(); + } + + public void enableShutter(boolean enabled) { + if (mShutterButton != null) { + mShutterButton.setEnabled(enabled); + } + } + + public void pressShutterButton() { + if (mShutterButton.isInTouchMode()) { + mShutterButton.requestFocusFromTouch(); + } else { + mShutterButton.requestFocus(); + } + mShutterButton.setPressed(true); + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + int newZoom = mController.onZoomChanged(index); + if (mZoomRenderer != null) { + mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom)); + } + } + + @Override + public void onZoomStart() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(true); + } + } + + @Override + public void onZoomEnd() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(false); + } + } + } + + @Override + public void onPieOpened(int centerX, int centerY) { + //TODO: mActivity.cancelActivityTouchHandling(); + //TODO: mActivity.setSwipingEnabled(false); + if (mFaceView != null) { + mFaceView.setBlockDraw(true); + } + } + + @Override + public void onPieClosed() { + //TODO: mActivity.setSwipingEnabled(true); + if (mFaceView != null) { + mFaceView.setBlockDraw(false); + } + } + + public Object getSurfaceTexture() { + synchronized (mLock) { + if (mSurfaceTexture == null) { + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "Unexpected interruption when waiting to get surface texture"); + } + } + } + return mSurfaceTexture; + } + + // Countdown timer + + private void initializeCountDown() { + mActivity.getLayoutInflater().inflate(R.layout.count_down_to_capture, + (ViewGroup) mRootView, true); + mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture)); + mCountDownView.setCountDownFinishedListener((OnCountDownFinishedListener) mController); + } + + public boolean isCountingDown() { + return mCountDownView != null && mCountDownView.isCountingDown(); + } + + public void cancelCountDown() { + if (mCountDownView == null) return; + mCountDownView.cancelCountDown(); + } + + public void startCountDown(int sec, boolean playSound) { + if (mCountDownView == null) initializeCountDown(); + mCountDownView.startCountDown(sec, playSound); + } + + public void showPreferencesToast() { + 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(); + } + + public void onPause() { + cancelCountDown(); + + // Clear UI. + collapseCameraControls(); + if (mFaceView != null) mFaceView.clear(); + + mPreviewWidth = 0; + mPreviewHeight = 0; + } + + // focus UI implementation + + private FocusIndicator getFocusIndicator() { + return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer; + } + + @Override + public boolean hasFaces() { + return (mFaceView != null && mFaceView.faceExists()); + } + + public void clearFaces() { + if (mFaceView != null) mFaceView.clear(); + } + + @Override + public void clearFocus() { + FocusIndicator indicator = getFocusIndicator(); + if (indicator != null) indicator.clear(); + } + + @Override + public void setFocusPosition(int x, int y) { + mPieRenderer.setFocus(x, y); + } + + @Override + public void onFocusStarted() { + getFocusIndicator().showStart(); + } + + @Override + public void onFocusSucceeded(boolean timeout) { + getFocusIndicator().showSuccess(timeout); + } + + @Override + public void onFocusFailed(boolean timeout) { + getFocusIndicator().showFail(timeout); + } + + @Override + public void pauseFaceDetection() { + if (mFaceView != null) mFaceView.pause(); + } + + @Override + public void resumeFaceDetection() { + if (mFaceView != null) mFaceView.resume(); + } + + public void onStartFaceDetection(int orientation, boolean mirror) { + mFaceView.clear(); + mFaceView.setVisibility(View.VISIBLE); + mFaceView.setDisplayOrientation(orientation); + mFaceView.setMirror(mirror); + mFaceView.resume(); + } + + @Override + public void onFaceDetection(Face[] faces, android.hardware.Camera camera) { + mFaceView.setFaces(faces); + } + + @Override + public void onSwipe(int direction) { + if (direction == PreviewGestures.DIR_UP) { + openMenu(); + } + } +} diff --git a/src/com/android/camera/NewPreviewGestures.java b/src/com/android/camera/NewPreviewGestures.java new file mode 100644 index 000000000..2718e55ae --- /dev/null +++ b/src/com/android/camera/NewPreviewGestures.java @@ -0,0 +1,358 @@ +package com.android.camera; + +/* + * 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. + */ + +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.PreviewGestures.SwipeListener; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.List; + +public class NewPreviewGestures + 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 static final int MODE_SWIPE = 5; + + public static final int DIR_UP = 0; + public static final int DIR_DOWN = 1; + public static final int DIR_LEFT = 2; + public static final int DIR_RIGHT = 3; + + private NewCameraActivity mActivity; + private SingleTapListener mTapListener; + private CancelEventListener mCancelEventListener; + private RenderOverlay mOverlay; + private PieRenderer mPie; + private ZoomRenderer mZoom; + private MotionEvent mDown; + private MotionEvent mCurrent; + private ScaleGestureDetector mScale; + private List<View> mReceivers; + private int mMode; + private int mSlop; + private int mTapTimeout; + private boolean mEnabled; + private boolean mZoomOnly; + private int mOrientation; + private int[] mLocation; + private SwipeListener mSwipeListener; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + if (msg.what == MSG_PIE) { + mMode = MODE_PIE; + openPie(); + cancelActivityTouchHandling(mDown); + } + } + }; + + public interface SingleTapListener { + public void onSingleTapUp(View v, int x, int y); + } + + public interface CancelEventListener { + public void onTouchEventCancelled(MotionEvent cancelEvent); + } + + interface SwipeListener { + public void onSwipe(int direction); + } + + public NewPreviewGestures(NewCameraActivity ctx, SingleTapListener tapListener, + ZoomRenderer zoom, PieRenderer pie, SwipeListener swipe) { + mActivity = ctx; + mTapListener = tapListener; + 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]; + mSwipeListener = swipe; + } + + public void setCancelEventListener(CancelEventListener listener) { + mCancelEventListener = listener; + } + + 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<View>(); + } + mReceivers.add(v); + } + + public void clearTouchReceivers() { + if (mReceivers != null) { + mReceivers.clear(); + } + } + + public boolean dispatchTouch(MotionEvent m) { + if (!mEnabled) { + return false; + } + mCurrent = m; + if (MotionEvent.ACTION_DOWN == m.getActionMasked()) { + if (checkReceivers(m)) { + mMode = MODE_MODULE; + return false; + } 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 false; + } + } else if (mMode == MODE_NONE) { + return false; + } else if (mMode == MODE_SWIPE) { + if (MotionEvent.ACTION_UP == m.getActionMasked()) { + mSwipeListener.onSwipe(getSwipeDirection(m)); + } + return true; + } 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 false; + } 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) { + mTapListener.onSingleTapUp(null, + (int) mDown.getX() - mOverlay.getWindowPositionX(), + (int) mDown.getY() - mOverlay.getWindowPositionY()); + return true; + } else { + return false; + } + } 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(); + int dir = getSwipeDirection(m); + if (dir == DIR_LEFT) { + mMode = MODE_MODULE; + return false; + } else { + cancelActivityTouchHandling(m); + mMode = MODE_NONE; + } + } + } + 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 int getSwipeDirection(MotionEvent m) { + float dx = 0; + float dy = 0; + switch (mOrientation) { + case 0: + dx = m.getX() - mDown.getX(); + dy = m.getY() - mDown.getY(); + break; + case 90: + dx = - (m.getY() - mDown.getY()); + dy = m.getX() - mDown.getX(); + break; + case 180: + dx = -(m.getX() - mDown.getX()); + dy = m.getY() - mDown.getY(); + break; + case 270: + dx = m.getY() - mDown.getY(); + dy = m.getX() - mDown.getX(); + break; + } + if (dx < 0 && (Math.abs(dy) / -dx < 2)) return DIR_LEFT; + if (dx > 0 && (Math.abs(dy) / dx < 2)) return DIR_RIGHT; + if (dy > 0) return DIR_DOWN; + return DIR_UP; + } + + 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) { + if (mCancelEventListener != null) { + mCancelEventListener.onTouchEventCancelled(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/NewVideoMenu.java b/src/com/android/camera/NewVideoMenu.java new file mode 100644 index 000000000..1dec27a47 --- /dev/null +++ b/src/com/android/camera/NewVideoMenu.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.app.Activity; +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; +import com.android.gallery3d.R; + +public class NewVideoMenu extends PieController + implements MoreSettingPopup.Listener, + ListPrefSettingPopup.Listener, + TimeIntervalPopup.Listener { + + private static String TAG = "CAM_VideoMenu"; + private static final int POS_WB = 1; + private static final int POS_SET = 2; + private static final int POS_FLASH = 3; + private static final int POS_SWITCH = 4; + + private NewVideoUI mUI; + 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; + private NewCameraActivity mActivity; + + public NewVideoMenu(NewCameraActivity activity, NewVideoUI ui, PieRenderer pie) { + super(activity, pie); + mUI = ui; + mActivity = activity; + } + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + mPopupStatus = POPUP_NONE; + + PieItem item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, POS_FLASH, 5); + mRenderer.addItem(item); + item = makeItem(CameraSettings.KEY_WHITE_BALANCE, POS_WB, 5); + mRenderer.addItem(item); + // camera switcher + item = makeItem(R.drawable.ic_switch_video_facing_holo_light); + item.setPosition(POS_SWITCH, 5); + 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); + // settings popup + 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.setPosition(POS_SET, 5); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) { + initializePopup(); + mPopupStatus = POPUP_FIRST_LEVEL; + } + mUI.showPopup(mPopup); + } + }); + mRenderer.addItem(item); + } + + @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) { + mUI.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) mUI.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); + mUI.dismissPopup(true); + mPopup = timeInterval; + } else { + ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate( + R.layout.list_pref_setting_popup, null, false); + basic.initialize(pref); + basic.setSettingChangedListener(this); + mUI.dismissPopup(true); + mPopup = basic; + } + mUI.showPopup(mPopup); + mPopupStatus = POPUP_SECOND_LEVEL; + } + +} diff --git a/src/com/android/camera/NewVideoModule.java b/src/com/android/camera/NewVideoModule.java new file mode 100644 index 000000000..3fc748593 --- /dev/null +++ b/src/com/android/camera/NewVideoModule.java @@ -0,0 +1,2341 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.annotation.TargetApi; +import android.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.KeyEvent; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.RotateTextToast; +import com.android.gallery3d.R; +import com.android.gallery3d.app.OrientationManager; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.util.AccessibilityUtils; +import com.android.gallery3d.util.UsageStatistics; + +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 NewVideoModule implements NewCameraModule, + VideoController, + CameraPreference.OnPreferenceChangedListener, + ShutterButton.OnShutterButtonListener, + MediaRecorder.OnErrorListener, + MediaRecorder.OnInfoListener, + EffectsRecorder.EffectsListener { + + 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 NewCameraActivity mActivity; + private boolean mPaused; + private int mCameraId; + private Parameters mParameters; + + private Boolean mCameraOpened = false; + + 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 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 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; + + 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 int mDesiredPreviewWidth; + private int mDesiredPreviewHeight; + private ContentResolver mContentResolver; + + private LocationManager mLocationManager; + private OrientationManager mOrientationManager; + + private VideoNamer mVideoNamer; + private Surface mSurface; + private int mPendingSwitchCameraId; + private boolean mOpenCameraFail; + private boolean mCameraDisabled; + private final Handler mHandler = new MainHandler(); + private NewVideoUI mUI; + private CameraProxy mCameraDevice; + + // 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 boolean mRestoreFlash; // This is used to check if we need to restore the flash + // status when going back from gallery. + + private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + Util.broadcastNewPicture(mActivity, uri); + } + } + }; + + + protected class CameraOpenThread extends Thread { + @Override + public void run() { + openCamera(); + } + } + + private void openCamera() { + try { + synchronized(mCameraOpened) { + if (!mCameraOpened) { + mCameraDevice = Util.openCamera(mActivity, mCameraId); + mCameraOpened = true; + } + } + mParameters = mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + mOpenCameraFail = true; + } catch (CameraDisabledException e) { + 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: + mUI.enableShutter(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: { + //TODO: + //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + + // Enable all camera controls. + mSwitchingCamera = false; + 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() { + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { // API level < 16 + mUI.initializeSurfaceView(); + } + } + + @Override + public void init(NewCameraActivity activity, View root) { + mActivity = activity; + mUI = new NewVideoUI(activity, this, root); + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mCameraId = getPreferredCameraId(mPreferences); + + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + + mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default); + resetEffect(); + mOrientationManager = new OrientationManager(mActivity); + + /* + * 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(); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsVideoCaptureIntent = isVideoCaptureIntent(); + initializeSurfaceView(); + + // Make sure camera device is opened. + try { + cameraOpenThread.join(); + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + } catch (InterruptedException ex) { + // ignore + } + + readVideoPreferences(); + mUI.setPrefChangedListener(this); + new Thread(new Runnable() { + @Override + public void run() { + startPreview(); + } + }).start(); + + mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); + mLocationManager = new LocationManager(mActivity, null); + + mUI.setOrientationIndicator(0, false); + setDisplayOrientation(); + + mUI.showTimeLapseUI(mCaptureTimeLapse); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + + initializeVideoControl(); + mPendingSwitchCameraId = -1; + mUI.updateOnScreenIndicators(mParameters); + + // 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()) { + mUI.enableShutter(false); + } + } + + // SingleTapListener + // 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; + } + + MediaSaveService s = mActivity.getMediaSaveService(); + if (mPaused || mSnapshotInProgress || effectsActive() || s == null || s.isQueueFull()) { + return; + } + + if (!mMediaRecorderRecording) { + // check for dismissing popup + mUI.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); + mCameraDevice.setParameters(mParameters); + + Log.v(TAG, "Video snapshot start"); + mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc)); + showVideoSnapshotUI(true); + mSnapshotInProgress = true; + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot"); + } + + @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)); + } + + private void initializeVideoControl() { + loadCameraPreferences(); + mUI.initializePopup(mPreferenceGroup); + if (effectsActive()) { + mUI.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 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) { + showCaptureResult(); + } + } + } 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. + // TODO: need to get the capture animation to work + // ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + } + } + } + + public void onProtectiveCurtainClick(View v) { + // Consume clicks + } + + @Override + public void onShutterButtonClick() { + if (mUI.collapseCameraControls() || mSwitchingCamera) return; + + boolean stop = mMediaRecorderRecording; + + if (stop) { + onStopVideoRecording(); + } else { + startVideoRecording(); + } + mUI.enableShutter(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) { + mUI.setShutterPressed(pressed); + } + + 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 = 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<Size> sizes = mParameters.getSupportedPreviewSizes(); + Size preferred = mParameters.getPreferredPreviewSizeForVideo(); + int product = preferred.width * preferred.height; + Iterator<Size> 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; + } + mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); + Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth + + ". mDesiredPreviewHeight=" + mDesiredPreviewHeight); + } + + private void resizeForPreviewAspectRatio() { + mUI.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 (mOpenCameraFail || mCameraDisabled) + return; + mUI.enableShutter(false); + mZoomValue = 0; + + showVideoSnapshotUI(false); + + if (!mPreviewing) { + resetEffect(); + openCamera(); + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, + R.string.cannot_connect_camera); + return; + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + readVideoPreferences(); + resizeForPreviewAspectRatio(); + new Thread(new Runnable() { + @Override + public void run() { + startPreview(); + } + }).start(); + } else { + // preview already started + mUI.enableShutter(true); + } + + // Initializing it here after the preview is started. + mUI.initializeZoom(mParameters); + + 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(); + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "VideoModule"); + } + + private void setDisplayOrientation() { + mDisplayRotation = Util.getDisplayRotation(mActivity); + mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId); + // Change the camera display orientation + if (mCameraDevice != null) { + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + } + } + + @Override + public int onZoomChanged(int index) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return index; + mZoomValue = index; + if (mParameters == null || mCameraDevice == null) return index; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mCameraDevice.setParametersAsync(mParameters); + Parameters p = mCameraDevice.getParameters(); + if (p != null) return p.getZoom(); + return index; + } + private void startPreview() { + Log.v(TAG, "startPreview"); + + mCameraDevice.setErrorCallback(mErrorCallback); + if (mPreviewing == true) { + stopPreview(); + if (effectsActive() && mEffectsRecorder != null) { + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + } + + setDisplayOrientation(); + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + setCameraParameters(); + + try { + if (!effectsActive()) { + SurfaceTexture surfaceTexture = mUI.getSurfaceTexture(); + if (surfaceTexture == null) { + return; // The texture has been destroyed (pause, etc) + } + mCameraDevice.setPreviewTextureAsync(surfaceTexture); + mCameraDevice.startPreviewAsync(); + mPreviewing = true; + onPreviewStarted(); + } else { + initializeEffectsPreview(); + mEffectsRecorder.startPreview(); + mPreviewing = true; + onPreviewStarted(); + } + } catch (Throwable ex) { + closeCamera(); + throw new RuntimeException("startPreview failed", ex); + } finally { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + } + } + }); + } + + } + + private void onPreviewStarted() { + mUI.enableShutter(true); + } + + @Override + public void stopPreview() { + if (!mPreviewing) return; + 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 (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(); + mCameraDevice.setZoomChangeListener(null); + mCameraDevice.setErrorCallback(null); + synchronized(mCameraOpened) { + if (mCameraOpened) { + CameraHolder.instance().release(); + } + mCameraOpened = false; + } + mCameraDevice = null; + mPreviewing = false; + mSnapshotInProgress = false; + } + + private void releasePreviewResources() { + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + mUI.hideSurfaceView(); + } + } + + @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 (mUI.hidePieRenderer()) { + return true; + } else { + return mUI.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) { + mUI.clickShutter(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + if (event.getRepeatCount() == 0) { + mUI.clickShutter(); + 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: + mUI.pressShutter(false); + return true; + } + return false; + } + + @Override + public 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_RECORDING) { + // We stop the preview here before unlocking the device because we + // need to change the SurfaceTexture to SurfaceView for preview. + stopPreview(); + mCameraDevice.setPreviewDisplayAsync(mUI.getSurfaceHolder()); + // 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. + mCameraDevice.setDisplayOrientation( + Util.getDisplayOrientation(mDisplayRotation, mCameraId)); + mCameraDevice.startPreviewAsync(); + mPreviewing = true; + mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface()); + } + } + + // Prepares media recorder. + private void initializeRecorder() { + Log.v(TAG, "initializeRecorder"); + // If the mCameraDevice is null, then this activity is going to finish + if (mCameraDevice == null) return; + + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // 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(). + mUI.showSurfaceView(); + } + + 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. + mCameraDevice.unlock(); + mCameraDevice.waitDone(); + mMediaRecorder.setCamera(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 (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(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); + + mEffectsRecorder.setPreviewSurfaceTexture(mUI.getSurfaceTexture(), + mUI.getPreviewWidth(), mUI.getPreviewHeight()); + + 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(); + //TODO: 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"); + // TODO: 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. + mCameraDevice.lock(); + return; + } + } + + // Make sure the video recording has started before announcing + // this in accessibility. + AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), + 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 = mCameraDevice.getParameters(); + } + + mUI.enableCameraControls(false); + + mMediaRecorderRecording = true; + mOrientationManager.lockOrientation(); + mRecordingStartTime = SystemClock.uptimeMillis(); + mUI.showRecordingUI(true, mParameters.isZoomSupported()); + + updateRecordingTime(); + keepScreenOn(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + UsageStatistics.ACTION_CAPTURE_START, "Video"); + } + + private void showCaptureResult() { + Bitmap bitmap = null; + if (mVideoFileDescriptor != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(), + mDesiredPreviewWidth); + } else if (mCurrentVideoFilename != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename, + mDesiredPreviewWidth); + } + 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); + mUI.showReviewImage(bitmap); + } + + mUI.showReviewControls(); + mUI.enableCameraControls(false); + mUI.showTimeLapseUI(false); + } + + private void hideAlert() { + mUI.enableCameraControls(true); + mUI.hideReviewUI(); + if (mCaptureTimeLapse) { + mUI.showTimeLapseUI(true); + } + } + + private boolean stopVideoRecording() { + Log.v(TAG, "stopVideoRecording"); + //TODO: mUI.setSwipingEnabled(true); + mUI.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(mUI.getShutterButton(), + 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; + mOrientationManager.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); + } + + mUI.showRecordingUI(false, mParameters.isZoomSupported()); + if (!mIsVideoCaptureIntent) { + mUI.enableCameraControls(true); + } + // The orientation was fixed during video recording. Now make it + // reflect the device orientation as video recording is stopped. + mUI.setOrientationIndicator(0, true); + keepScreenOnAwhile(); + if (shouldAddToMediaStoreNow) { + if (addVideoToMediaStore()) fail = true; + } + } + // always release media recorder if no effects running + if (!effectsActive()) { + releaseMediaRecorder(); + if (!mPaused) { + mCameraDevice.lock(); + mCameraDevice.waitDone(); + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + stopPreview(); + mUI.hideSurfaceView(); + // Switch back to use SurfaceTexture for preview. + startPreview(); + } + } + } + // Update the parameters here because the parameters might have been altered + // by MediaRecorder. + if (!mPaused) mParameters = mCameraDevice.getParameters(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + fail ? UsageStatistics.ACTION_CAPTURE_FAIL : + UsageStatistics.ACTION_CAPTURE_DONE, "Video", + SystemClock.uptimeMillis() - mRecordingStartTime); + 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; + } + + mUI.setRecordingTime(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); + + mUI.setRecordingTimeTextColor(color); + } + + long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay); + mHandler.sendEmptyMessageDelayed( + UPDATE_RECORD_TIME, actualNextUpdateDelay); + } + + private static boolean isSupported(String value, List<String> 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 (mUI.isVisible()) { + flashMode = mPreferences.getString( + CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, + mActivity.getString(R.string.pref_camera_video_flashmode_default)); + } else { + flashMode = Parameters.FLASH_MODE_OFF; + } + List<String> 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<String> 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<Size> 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); + + mCameraDevice.setParameters(mParameters); + // Keep preview size up to date. + mParameters = mCameraDevice.getParameters(); + } + + @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. + 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 { + showCaptureResult(); + } + } + } + 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. + mUI.enableShutter(true); + } + // 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) { + // 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); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged"); + setDisplayOrientation(); + } + + @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 (mCameraDevice == null) return; + + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + // Check if the current effects selection has changed + if (updateEffectSelection()) return; + + readVideoPreferences(); + mUI.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(); + } + mUI.updateOnScreenIndicators(mParameters); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + private void switchCamera() { + if (mPaused) return; + + Log.d(TAG, "Start to switch camera."); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + setCameraId(mCameraId); + + closeCamera(); + mUI.collapseCameraControls(); + // 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 + mUI.initializeZoom(mParameters); + mUI.setOrientationIndicator(0, false); + + // Start switch camera animation. Post a message because + // onFrameAvailable from the old camera may already exist. + mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); + mUI.updateOnScreenIndicators(mParameters); + } + + // 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(); + mUI.showTimeLapseUI(mCaptureTimeLapse); + Size size = mParameters.getPreviewSize(); + if (size.width != mDesiredPreviewWidth + || size.height != mDesiredPreviewHeight) { + resizeForPreviewAspectRatio(); + } + // Start up preview again + startPreview(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mSwitchingCamera) return true; + return mUI.dispatchTouchEvent(m); + } + + private void initializeVideoSnapshot() { + if (mParameters == null) return; + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + // 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); + } + } + } + + void showVideoSnapshotUI(boolean enabled) { + if (mParameters == null) return; + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + if (enabled) { + // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + } else { + mUI.showPreviewBorder(enabled); + } + mUI.enableShutter(!enabled); + } + } + + @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 (!mUI.isVisible()) { + if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) { + mRestoreFlash = false; + return; + } + mRestoreFlash = true; + setCameraParameters(); + } else if (mRestoreFlash) { + mRestoreFlash = false; + setCameraParameters(); + } + } + + @Override + public void onFullScreenChanged(boolean full) { + mUI.onFullScreenChanged(full); + } + + 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); + ExifInterface exif = Exif.getExif(data); + int orientation = Exif.getOrientation(exif); + Size s = mParameters.getPictureSize(); + mActivity.getMediaSaveService().addImage( + data, title, dateTaken, loc, s.width, s.height, orientation, + exif, mOnMediaSavedListener, mContentResolver); + } + + 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; + } + } + + @Override + public boolean updateStorageHintOnResume() { + return true; + } + + // required by OnPreferenceChangedListener + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + 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. + // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); + // Disable all camera controls. + mSwitchingCamera = true; + + } + + @Override + public boolean needsSwitcher() { + return !mIsVideoCaptureIntent; + } + + @Override + public boolean needsPieMenu() { + return true; + } + + @Override + public void onShowSwitcherPopup() { + mUI.onShowSwitcherPopup(); + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // do nothing. + } +} diff --git a/src/com/android/camera/NewVideoUI.java b/src/com/android/camera/NewVideoUI.java new file mode 100644 index 000000000..6dada60cd --- /dev/null +++ b/src/com/android/camera/NewVideoUI.java @@ -0,0 +1,716 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CameraSwitcher; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.RotateLayout; +import com.android.camera.ui.ZoomRenderer; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.List; + +public class NewVideoUI implements PieRenderer.PieListener, + NewPreviewGestures.SingleTapListener, + NewPreviewGestures.SwipeListener, SurfaceTextureListener, + SurfaceHolder.Callback { + private final static String TAG = "CAM_VideoUI"; + private static final int UPDATE_TRANSFORM_MATRIX = 1; + // module fields + private NewCameraActivity mActivity; + private View mRootView; + private TextureView mTextureView; + // An review image having same size as preview. It is displayed when + // recording is stopped in capture intent. + private ImageView mReviewImage; + private View mReviewCancelButton; + private View mReviewDoneButton; + private View mReviewPlayButton; + private ShutterButton mShutterButton; + private CameraSwitcher mSwitcher; + private TextView mRecordingTimeView; + private LinearLayout mLabelsLinearLayout; + private View mTimeLapseLabel; + private RenderOverlay mRenderOverlay; + private PieRenderer mPieRenderer; + private NewVideoMenu mVideoMenu; + private View mCameraControls; + private AbstractSettingPopup mPopup; + private ZoomRenderer mZoomRenderer; + private NewPreviewGestures mGestures; + private View mMenuButton; + private View mBlocker; + private View mOnScreenIndicators; + private ImageView mFlashIndicator; + private RotateLayout mRecordingTimeRect; + private final Object mLock = new Object(); + private SurfaceTexture mSurfaceTexture; + private VideoController mController; + private int mZoomMax; + private List<Integer> mZoomRatios; + + private SurfaceView mSurfaceView = null; + private int mPreviewWidth = 0; + private int mPreviewHeight = 0; + private float mSurfaceTextureUncroppedWidth; + private float mSurfaceTextureUncroppedHeight; + private float mAspectRatio = 4f / 3f; + private Matrix mMatrix = null; + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_TRANSFORM_MATRIX: + setTransformMatrix(mPreviewWidth, mPreviewHeight); + break; + default: + break; + } + } + }; + private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + int width = right - left; + int height = bottom - top; + // Full-screen screennail + int w = width; + int h = height; + if (Util.getDisplayRotation(mActivity) % 180 != 0) { + w = height; + h = width; + } + if (mPreviewWidth != width || mPreviewHeight != height) { + mPreviewWidth = width; + mPreviewHeight = height; + onScreenSizeChanged(width, height, w, h); + } + } + }; + + public NewVideoUI(NewCameraActivity activity, VideoController controller, View parent) { + mActivity = activity; + mController = controller; + mRootView = parent; + mActivity.getLayoutInflater().inflate(R.layout.new_video_module, (ViewGroup) mRootView, true); + mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content); + mTextureView.setSurfaceTextureListener(this); + mTextureView.addOnLayoutChangeListener(mLayoutListener); + mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); + mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(1); + mSwitcher.setSwitchListener((CameraSwitchListener) mActivity); + initializeMiscControls(); + initializeControlByIntent(); + initializeOverlay(); + } + + + public void initializeSurfaceView() { + mSurfaceView = new SurfaceView(mActivity); + ((ViewGroup) mRootView).addView(mSurfaceView, 0); + mSurfaceView.getHolder().addCallback(this); + } + + private void initializeControlByIntent() { + mBlocker = mActivity.findViewById(R.id.blocker); + mMenuButton = mActivity.findViewById(R.id.menu); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mPieRenderer != null) { + mPieRenderer.showInCenter(); + } + } + }); + + mCameraControls = mActivity.findViewById(R.id.camera_controls); + mOnScreenIndicators = mActivity.findViewById(R.id.on_screen_indicators); + mFlashIndicator = (ImageView) mActivity.findViewById(R.id.menu_flash_indicator); + if (mController.isVideoCaptureIntent()) { + hideSwitcher(); + mActivity.getLayoutInflater().inflate(R.layout.review_module_control, (ViewGroup) mCameraControls); + // Cannot use RotateImageView for "done" and "cancel" button because + // the tablet layout uses RotateLayout, which cannot be cast to + // RotateImageView. + mReviewDoneButton = mActivity.findViewById(R.id.btn_done); + mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel); + mReviewPlayButton = mActivity.findViewById(R.id.btn_play); + mReviewCancelButton.setVisibility(View.VISIBLE); + mReviewDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewDoneClicked(v); + } + }); + mReviewCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewCancelClicked(v); + } + }); + mReviewPlayButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewPlayClicked(v); + } + }); + } + } + + public void setPreviewSize(int width, int height) { + if (width == 0 || height == 0) { + Log.w(TAG, "Preview size should not be 0."); + return; + } + if (width > height) { + mAspectRatio = (float) width / height; + } else { + mAspectRatio = (float) height / width; + } + mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX); + } + + public int getPreviewWidth() { + return mPreviewWidth; + } + + public int getPreviewHeight() { + return mPreviewHeight; + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + setTransformMatrix(width, height); + } + + private void setTransformMatrix(int width, int height) { + mMatrix = mTextureView.getTransform(mMatrix); + int orientation = Util.getDisplayRotation(mActivity); + float scaleX = 1f, scaleY = 1f; + float scaledTextureWidth, scaledTextureHeight; + if (width > height) { + scaledTextureWidth = Math.max(width, + (int) (height * mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int)(width / mAspectRatio)); + } else { + scaledTextureWidth = Math.max(width, + (int) (height / mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int) (width * mAspectRatio)); + } + + if (mSurfaceTextureUncroppedWidth != scaledTextureWidth || + mSurfaceTextureUncroppedHeight != scaledTextureHeight) { + mSurfaceTextureUncroppedWidth = scaledTextureWidth; + mSurfaceTextureUncroppedHeight = scaledTextureHeight; + } + scaleX = scaledTextureWidth / width; + scaleY = scaledTextureHeight / height; + mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2); + mTextureView.setTransform(mMatrix); + } + + public void hideUI() { + mCameraControls.setVisibility(View.INVISIBLE); + hideSwitcher(); + mShutterButton.setVisibility(View.GONE); + } + + public void showUI() { + mCameraControls.setVisibility(View.VISIBLE); + showSwitcher(); + mShutterButton.setVisibility(View.VISIBLE); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void showSwitcher() { + mSwitcher.setVisibility(View.VISIBLE); + } + + 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; + } + + public void enableCameraControls(boolean enable) { + if (mGestures != null) { + mGestures.setZoomOnly(!enable); + } + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + + public void overrideSettings(final String... keyvalues) { + mVideoMenu.overrideSettings(keyvalues); + } + + public void setOrientationIndicator(int orientation, boolean animation) { + if (mGestures != null) { + mGestures.setOrientation(orientation); + } + // 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); + } + + public SurfaceHolder getSurfaceHolder() { + return mSurfaceView.getHolder(); + } + + public void hideSurfaceView() { + mSurfaceView.setVisibility(View.GONE); + mTextureView.setVisibility(View.VISIBLE); + setTransformMatrix(mPreviewWidth, mPreviewHeight); + } + + public void showSurfaceView() { + // TODO: Need to transform the surface view + mSurfaceView.setVisibility(View.VISIBLE); + mTextureView.setVisibility(View.GONE); + } + + private void initializeOverlay() { + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mVideoMenu = new NewVideoMenu(mActivity, this, mPieRenderer); + mPieRenderer.setPieListener(this); + } + mRenderOverlay.addRenderer(mPieRenderer); + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + } + mRenderOverlay.addRenderer(mZoomRenderer); + if (mGestures == null) { + mGestures = new NewPreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer, this); + } + mGestures.setRenderOverlay(mRenderOverlay); + mGestures.clearTouchReceivers(); + mGestures.addTouchReceiver(mMenuButton); + mGestures.addTouchReceiver(mBlocker); + if (mController.isVideoCaptureIntent()) { + if (mReviewCancelButton != null) { + mGestures.addTouchReceiver(mReviewCancelButton); + } + if (mReviewDoneButton != null) { + mGestures.addTouchReceiver(mReviewDoneButton); + } + if (mReviewPlayButton != null) { + mGestures.addTouchReceiver(mReviewPlayButton); + } + } + } + + public void setPrefChangedListener(OnPreferenceChangedListener listener) { + mVideoMenu.setListener(listener); + } + + private void initializeMiscControls() { + mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image); + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + mShutterButton.setOnShutterButtonListener(mController); + mShutterButton.setVisibility(View.VISIBLE); + mShutterButton.requestFocus(); + mShutterButton.enableTouch(true); + 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); + } + + public void updateOnScreenIndicators(Parameters param) { + if (param == null) return; + String value = param.getFlashMode(); + 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); + } + } + } + + public void setAspectRatio(double ratio) { + // mPreviewFrameLayout.setAspectRatio(ratio); + } + + public void showTimeLapseUI(boolean enable) { + if (mTimeLapseLabel != null) { + mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE); + } + } + + private void openMenu() { + if (mPieRenderer != null) { + mPieRenderer.showInCenter(); + } + } + + public void showPopup(AbstractSettingPopup popup) { + 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) { + showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mVideoMenu.popupDismissed(topLevelPopupOnly); + } + + public void onShowSwitcherPopup() { + hidePieRenderer(); + } + + public boolean hidePieRenderer() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + return true; + } + return false; + } + + // disable preview gestures after shutter is pressed + public void setShutterPressed(boolean pressed) { + if (mGestures == null) return; + mGestures.setEnabled(!pressed); + } + + public void enableShutter(boolean enable) { + if (mShutterButton != null) { + mShutterButton.setEnabled(enable); + } + } + + // PieListener + @Override + public void onPieOpened(int centerX, int centerY) { + // TODO: mActivity.cancelActivityTouchHandling(); + // mActivity.setSwipingEnabled(false); + } + + @Override + public void onPieClosed() { + // TODO: mActivity.setSwipingEnabled(true); + } + + public void showPreviewBorder(boolean enable) { + // TODO: mPreviewFrameLayout.showBorder(enable); + } + + // SingleTapListener + // Preview area is touched. Take a picture. + @Override + public void onSingleTapUp(View view, int x, int y) { + mController.onSingleTapUp(view, x, y); + } + + // SurfaceView callback + @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"); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v(TAG, "Surface destroyed"); + mController.stopPreview(); + } + + public void showRecordingUI(boolean recording, boolean zoomSupported) { + mMenuButton.setVisibility(recording ? View.GONE : View.VISIBLE); + mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE); + if (recording) { + mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording); + hideSwitcher(); + mRecordingTimeView.setText(""); + mRecordingTimeView.setVisibility(View.VISIBLE); + // 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 && zoomSupported) { + // TODO: disable zoom UI here. + } + } else { + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + showSwitcher(); + mRecordingTimeView.setVisibility(View.GONE); + if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) { + // TODO: enable zoom UI here. + } + } + } + + public void showReviewImage(Bitmap bitmap) { + mReviewImage.setImageBitmap(bitmap); + mReviewImage.setVisibility(View.VISIBLE); + } + + public void showReviewControls() { + Util.fadeOut(mShutterButton); + Util.fadeIn(mReviewDoneButton); + Util.fadeIn(mReviewPlayButton); + mReviewImage.setVisibility(View.VISIBLE); + mMenuButton.setVisibility(View.GONE); + mOnScreenIndicators.setVisibility(View.GONE); + } + + public void hideReviewUI() { + mReviewImage.setVisibility(View.GONE); + mShutterButton.setEnabled(true); + mMenuButton.setVisibility(View.VISIBLE); + mOnScreenIndicators.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewDoneButton); + Util.fadeOut(mReviewPlayButton); + Util.fadeIn(mShutterButton); + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenuButton != null) { + mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + 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); + } + } + + public void initializePopup(PreferenceGroup pref) { + mVideoMenu.initialize(pref); + } + + public void initializeZoom(Parameters param) { + if (param == null || !param.isZoomSupported()) return; + mZoomMax = param.getMaxZoom(); + mZoomRatios = param.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(param.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(param.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + + public void clickShutter() { + mShutterButton.performClick(); + } + + public void pressShutter(boolean pressed) { + mShutterButton.setPressed(pressed); + } + + public View getShutterButton() { + return mShutterButton; + } + + // Gestures and touch events + + public boolean dispatchTouchEvent(MotionEvent m) { + if (mPopup != null || mSwitcher.showsPopup()) { + boolean handled = mRootView.dispatchTouchEvent(m); + if (!handled && mPopup != null) { + dismissPopup(false); + } + return handled; + } else if (mGestures != null && mRenderOverlay != null) { + if (mGestures.dispatchTouch(m)) { + return true; + } else { + return mRootView.dispatchTouchEvent(m); + } + } + return true; + } + public void setRecordingTime(String text) { + mRecordingTimeView.setText(text); + } + + public void setRecordingTimeTextColor(int color) { + mRecordingTimeView.setTextColor(color); + } + + public boolean isVisible() { + return mTextureView.getVisibility() == View.VISIBLE; + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + int newZoom = mController.onZoomChanged(index); + if (mZoomRenderer != null) { + mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom)); + } + } + + @Override + public void onZoomStart() { + } + + @Override + public void onZoomEnd() { + } + } + + @Override + public void onSwipe(int direction) { + if (direction == PreviewGestures.DIR_UP) { + openMenu(); + } + } + + public SurfaceTexture getSurfaceTexture() { + synchronized (mLock) { + if (mSurfaceTexture == null) { + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "Unexpected interruption when waiting to get surface texture"); + } + } + } + return mSurfaceTexture; + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + synchronized (mLock) { + mSurfaceTexture = surface; + mLock.notifyAll(); + } + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mSurfaceTexture = null; + mController.stopPreview(); + Log.d(TAG, "surfaceTexture is destroyed"); + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + +} diff --git a/src/com/android/camera/PhotoMenu.java b/src/com/android/camera/PhotoMenu.java index f9400fc99..f5ba733c9 100644 --- a/src/com/android/camera/PhotoMenu.java +++ b/src/com/android/camera/PhotoMenu.java @@ -51,11 +51,13 @@ public class PhotoMenu extends PieController private MoreSettingPopup mPopup; // Second level popup private AbstractSettingPopup mSecondPopup; + private CameraActivity mActivity; public PhotoMenu(CameraActivity activity, PhotoUI ui, PieRenderer pie) { super(activity, pie); mUI = ui; mSettingOff = activity.getString(R.string.setting_off_value); + mActivity = activity; } public void initialize(PreferenceGroup group) { diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java index c5d1b8b41..a9b8575ef 100644 --- a/src/com/android/camera/PieController.java +++ b/src/com/android/camera/PieController.java @@ -16,6 +16,7 @@ package com.android.camera; +import android.app.Activity; import android.graphics.drawable.Drawable; import android.util.Log; @@ -37,7 +38,10 @@ public class PieController { protected static final int MODE_PHOTO = 0; protected static final int MODE_VIDEO = 1; - protected CameraActivity mActivity; + protected static float CENTER = (float) Math.PI / 2; + protected static final float SWEEP = 0.06f; + + protected Activity mActivity; protected PreferenceGroup mPreferenceGroup; protected OnPreferenceChangedListener mListener; protected PieRenderer mRenderer; @@ -49,7 +53,7 @@ public class PieController { mListener = listener; } - public PieController(CameraActivity activity, PieRenderer pie) { + public PieController(Activity activity, PieRenderer pie) { mActivity = activity; mRenderer = pie; mPreferences = new ArrayList<IconListPreference>(); diff --git a/src/com/android/camera/VideoMenu.java b/src/com/android/camera/VideoMenu.java index d9629d3ac..c9f293222 100644 --- a/src/com/android/camera/VideoMenu.java +++ b/src/com/android/camera/VideoMenu.java @@ -47,10 +47,12 @@ public class VideoMenu extends PieController private static final int POPUP_FIRST_LEVEL = 1; private static final int POPUP_SECOND_LEVEL = 2; private int mPopupStatus; + private CameraActivity mActivity; public VideoMenu(CameraActivity activity, VideoUI ui, PieRenderer pie) { super(activity, pie); mUI = ui; + mActivity = activity; } public void initialize(PreferenceGroup group) { diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java index bb615e9ec..43822e7e0 100644 --- a/src/com/android/camera/VideoUI.java +++ b/src/com/android/camera/VideoUI.java @@ -52,7 +52,6 @@ public class VideoUI implements SurfaceHolder.Callback, PieRenderer.PieListener, private View mRootView; private PreviewFrameLayout mPreviewFrameLayout; private boolean mSurfaceViewReady; - private SurfaceHolder.Callback mSurfaceViewCallback; private PreviewSurfaceView mPreviewSurfaceView; // An review image having same size as preview. It is displayed when // recording is stopped in capture intent. diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java index b046ff7ae..fee28a376 100644 --- a/src/com/android/camera/ui/CameraSwitcher.java +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -38,6 +38,7 @@ import android.widget.LinearLayout; import com.android.camera.Util; import com.android.gallery3d.R; import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; import com.android.gallery3d.util.UsageStatistics; public class CameraSwitcher extends RotateImageView @@ -46,6 +47,16 @@ public class CameraSwitcher extends RotateImageView private static final String TAG = "CAM_Switcher"; private static final int SWITCHER_POPUP_ANIM_DURATION = 200; + public static final int PHOTO_MODULE_INDEX = 0; + public static final int VIDEO_MODULE_INDEX = 1; + public static final int PANORAMA_MODULE_INDEX = 2; + public static final int LIGHTCYCLE_MODULE_INDEX = 3; + private static final int[] DRAW_IDS = { + R.drawable.ic_switch_camera, + R.drawable.ic_switch_video, + R.drawable.ic_switch_pan, + R.drawable.ic_switch_photosphere + }; public interface CameraSwitchListener { public void onCameraSelected(int i); public void onShowSwitcherPopup(); @@ -82,6 +93,28 @@ public class CameraSwitcher extends RotateImageView mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); setOnClickListener(this); mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); + initializeDrawables(context); + } + + public void initializeDrawables(Context context) { + int totaldrawid = (LightCycleHelper.hasLightCycleCapture(context) + ? 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 < DRAW_IDS.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(context)) { + continue; // not enabled, so don't add to UI + } + moduleids[ix] = i; + drawids[ix++] = DRAW_IDS[i]; + } + setIds(moduleids, drawids); } public void setIds(int[] moduleids, int[] drawids) { diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java index 099f3bf4b..9840b1544 100644 --- a/src/com/android/camera/ui/FaceView.java +++ b/src/com/android/camera/ui/FaceView.java @@ -33,12 +33,15 @@ import android.view.View; import com.android.camera.CameraActivity; import com.android.camera.CameraScreenNail; +import com.android.camera.NewPhotoUI; import com.android.camera.Util; import com.android.gallery3d.R; import com.android.gallery3d.common.ApiHelper; @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) -public class FaceView extends View implements FocusIndicator, Rotatable { +public class FaceView extends View + implements FocusIndicator, Rotatable, + NewPhotoUI.SurfaceTextureSizeChangedListener { private static final String TAG = "CAM FaceView"; private final boolean LOGV = false; // The value for android.hardware.Camera.setDisplayOrientation. @@ -63,6 +66,8 @@ public class FaceView extends View implements FocusIndicator, Rotatable { private Paint mPaint; private volatile boolean mBlocked; + private int mUncroppedWidth; + private int mUncroppedHeight; private static final int MSG_SWITCH_FACES = 1; private static final int SWITCH_DELAY = 70; private boolean mStateSwitchPending = false; @@ -92,6 +97,11 @@ public class FaceView extends View implements FocusIndicator, Rotatable { mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke)); } + public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) { + mUncroppedWidth = uncroppedWidth; + mUncroppedHeight = uncroppedHeight; + } + public void setFaces(Face[] faces) { if (LOGV) Log.v(TAG, "Num of faces=" + faces.length); if (mPause) return; @@ -178,9 +188,17 @@ public class FaceView extends View implements FocusIndicator, Rotatable { @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(); + int rw, rh; + if (mUncroppedWidth == 0) { + // TODO: This check is temporary. It needs to be removed after the + // refactoring is fully functioning. + final CameraScreenNail sn = ((CameraActivity) getContext()).getCameraScreenNail(); + rw = sn.getUncroppedRenderWidth(); + rh = sn.getUncroppedRenderHeight(); + } else { + rw = mUncroppedWidth; + rh = mUncroppedHeight; + } // Prepare the matrix. if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180))) || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) { diff --git a/src/com/android/camera/ui/NewCameraRootView.java b/src/com/android/camera/ui/NewCameraRootView.java new file mode 100644 index 000000000..2d683bc3b --- /dev/null +++ b/src/com/android/camera/ui/NewCameraRootView.java @@ -0,0 +1,92 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.Util; +import com.android.gallery3d.R; + +public class NewCameraRootView extends FrameLayout + implements RotatableLayout.RotationListener { + + private int mOffset = 0; + public NewCameraRootView(Context context, AttributeSet attrs) { + super(context, attrs); + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + super.fitSystemWindows(insets); + // insets include status bar, navigation bar, etc + // In this case, we are only concerned with the size of nav bar + if (mOffset > 0) return true; + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); + if (insets.bottom > 0) { + mOffset = insets.bottom; + } else if (insets.right > 0) { + mOffset = insets.right; + } + Configuration config = getResources().getConfiguration(); + if (config.orientation == Configuration.ORIENTATION_PORTRAIT) { + lp.setMargins(0, 0, 0, mOffset); + } else if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { + lp.setMargins(0, 0, mOffset, 0); + } + CameraControls controls = (CameraControls) findViewById(R.id.camera_controls); + if (controls != null) { + controls.setRotationListener(this); + controls.adjustControlsToRightPosition(); + } + return true; + } + + public void cameraModuleChanged() { + CameraControls controls = (CameraControls) findViewById(R.id.camera_controls); + if (controls != null) { + controls.setRotationListener(this); + controls.adjustControlsToRightPosition(); + } + } + + @Override + public void onRotation(int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); + int b = lp.bottomMargin; + int t = lp.topMargin; + int l = lp.leftMargin; + int r = lp.rightMargin; + rotation = (rotation + 360) % 360; + if (rotation == 90) { + lp.setMargins(b, l, t, r); + } else if (rotation == 270) { + lp.setMargins(t, r, b, l); + } else if (rotation == 180) { + lp.setMargins(r, b, l, t); + } + } +} |