diff options
62 files changed, 9825 insertions, 113 deletions
diff --git a/src/com/android/camera/NewCameraActivity.java b/src/com/android/camera/NewCameraActivity.java new file mode 100644 index 000000000..e8d2157da --- /dev/null +++ b/src/com/android/camera/NewCameraActivity.java @@ -0,0 +1,346 @@ +/* + * 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.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.IBinder; +import android.provider.Settings; +import android.view.LayoutInflater; +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.data.CameraDataAdapter; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.FilmStripView; +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 CameraDataAdapter mDataAdapter; + private int mCurrentModuleIndex; + private NewCameraModule mCurrentModule; + private View mRootView; + private FilmStripView mFilmStripView; + 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_filmstrip); + 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); + } + }*/ + LayoutInflater inflater = getLayoutInflater(); + View rootLayout = inflater.inflate(R.layout.camera, null, false); + mRootView = rootLayout.findViewById(R.id.camera_app_root); + mDataAdapter = new CameraDataAdapter( + new ColorDrawable(getResources().getColor(R.color.photo_placeholder))); + mFilmStripView = (FilmStripView) findViewById(R.id.filmstrip_view); + // Set up the camera preview first so the preview shows up ASAP. + mDataAdapter.setCameraPreviewInfo(rootLayout, + FilmStripView.ImageData.SIZE_FULL, FilmStripView.ImageData.SIZE_FULL); + mFilmStripView.setDataAdapter(mDataAdapter); + 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(); + + // The loading is done in background and will update the filmstrip later. + mDataAdapter.requestLoad(getContentResolver()); + } + + @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) { + //if (mFilmStripView.isInCameraFullscreen()) { + // return mCurrentModule.dispatchTouchEvent(m); + //} + return mFilmStripView.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() { + } +} 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..659e2be19 --- /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.crop.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.setParameters(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..d1470b2b6 --- /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 = mRootView.findViewById(R.id.on_screen_indicators); + mExposureIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_exposure_indicator); + mFlashIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_flash_indicator); + mSceneIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_scenemode_indicator); + mHdrIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_hdr_indicator); + } + + 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 = mRootView.findViewById(R.id.blocker); + mMenuButton = mRootView.findViewById(R.id.menu); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openMenu(); + } + }); + if (mController.isImageCaptureIntent()) { + hideSwitcher(); + ViewGroup cameraControls = (ViewGroup) mRootView.findViewById(R.id.camera_controls); + mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls); + + mReviewDoneButton = mRootView.findViewById(R.id.btn_done); + mReviewCancelButton = mRootView.findViewById(R.id.btn_cancel); + mReviewRetakeButton = mRootView.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..6ff8bbf18 --- /dev/null +++ b/src/com/android/camera/NewVideoModule.java @@ -0,0 +1,2344 @@ +/* + * 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.setParameters(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 might have been altered by MediaRecorder already. + // We need to force mCameraDevice to refresh before getting it. + mCameraDevice.refreshParameters(); + // 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..a14dae3d3 --- /dev/null +++ b/src/com/android/camera/NewVideoUI.java @@ -0,0 +1,724 @@ +/* + * 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); + mRootView.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); + + if (mSurfaceView != null && mSurfaceView.getVisibility() == View.VISIBLE) { + LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams(); + lp.width = (int) mSurfaceTextureUncroppedWidth; + lp.height = (int) mSurfaceTextureUncroppedHeight; + lp.gravity = Gravity.CENTER; + mSurfaceView.requestLayout(); + } + } + + 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() { + mSurfaceView.setVisibility(View.VISIBLE); + mTextureView.setVisibility(View.GONE); + setTransformMatrix(mPreviewWidth, mPreviewHeight); + } + + 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); + } + + 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; + } + + // SurfaceTexture callbacks + @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) { + } + + // SurfaceHolder callbacks + @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(); + } +} diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java new file mode 100644 index 000000000..cb67aacd7 --- /dev/null +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -0,0 +1,604 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadataRetriever; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + +import com.android.camera.Storage; +import com.android.camera.ui.FilmStripView; +import com.android.camera.ui.FilmStripView.ImageData; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * A FilmStripDataProvider that provide data in the camera folder. + * + * The given view for camera preview won't be added until the preview info + * has been set by setCameraPreviewInfo(int, int). + */ +public class CameraDataAdapter implements FilmStripView.DataAdapter { + private static final String TAG = CameraDataAdapter.class.getSimpleName(); + + private static final int DEFAULT_DECODE_SIZE = 3000; + private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" }; + + private List<LocalData> mImages; + + private Listener mListener; + private View mCameraPreviewView; + private Drawable mPlaceHolder; + + private int mSuggestedWidth = DEFAULT_DECODE_SIZE; + private int mSuggestedHeight = DEFAULT_DECODE_SIZE; + + public CameraDataAdapter(Drawable placeHolder) { + mPlaceHolder = placeHolder; + } + + public void setCameraPreviewInfo(View cameraPreview, int width, int height) { + mCameraPreviewView = cameraPreview; + addOrReplaceCameraData(buildCameraImageData(width, height)); + } + + public void requestLoad(ContentResolver resolver) { + QueryTask qtask = new QueryTask(); + qtask.execute(resolver); + } + + @Override + public int getTotalNumber() { + if (mImages == null) return 0; + return mImages.size(); + } + + @Override + public ImageData getImageData(int id) { + if (mImages == null || id >= mImages.size()) return null; + return mImages.get(id); + } + + @Override + public void suggestSize(int w, int h) { + if (w <= 0 || h <= 0) { + mSuggestedWidth = mSuggestedHeight = DEFAULT_DECODE_SIZE; + } else { + mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE); + mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE); + } + } + + @Override + public View getView(Context c, int dataID) { + if (mImages == null) return null; + if (dataID >= mImages.size() || dataID < 0) { + return null; + } + + return mImages.get(dataID).getView( + c, mSuggestedWidth, mSuggestedHeight, mPlaceHolder); + } + + @Override + public void setListener(Listener listener) { + mListener = listener; + if (mImages != null) mListener.onDataLoaded(); + } + + private LocalData buildCameraImageData(int width, int height) { + LocalData d = new CameraPreviewData(width, height); + return d; + } + + private void addOrReplaceCameraData(LocalData data) { + if (mImages == null) mImages = new ArrayList<LocalData>(); + if (mImages.size() == 0) { + // No data at all. + mImages.add(0, data); + if (mListener != null) mListener.onDataLoaded(); + return; + } + + LocalData first = mImages.get(0); + if (first.getType() == ImageData.TYPE_CAMERA_PREVIEW) { + // Replace the old camera data. + mImages.set(0, data); + if (mListener != null) { + mListener.onDataUpdated(new UpdateReporter() { + @Override + public boolean isDataRemoved(int id) { + return false; + } + + @Override + public boolean isDataUpdated(int id) { + if (id == 0) return true; + return false; + } + }); + } + } else { + // Add a new camera data. + mImages.add(0, data); + if (mListener != null) { + mListener.onDataLoaded(); + } + } + } + + private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> { + @Override + protected List<LocalData> doInBackground(ContentResolver... resolver) { + List<LocalData> l = new ArrayList<LocalData>(); + // Photos + Cursor c = resolver[0].query( + Images.Media.EXTERNAL_CONTENT_URI, + LocalPhotoData.QUERY_PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + LocalPhotoData.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + // build up the list. + while (true) { + LocalData data = LocalPhotoData.buildFromCursor(c); + if (data != null) { + l.add(data); + } else { + Log.e(TAG, "Error loading data:" + + c.getString(LocalPhotoData.COL_DATA)); + } + if (c.isLast()) break; + c.moveToNext(); + } + } + if (c != null) c.close(); + + c = resolver[0].query( + Video.Media.EXTERNAL_CONTENT_URI, + LocalVideoData.QUERY_PROJECTION, + MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH, + LocalVideoData.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + // build up the list. + c.moveToFirst(); + while (true) { + LocalData data = LocalVideoData.buildFromCursor(c); + if (data != null) { + l.add(data); + Log.v(TAG, "video data added:" + data); + } else { + Log.e(TAG, "Error loading data:" + + c.getString(LocalVideoData.COL_DATA)); + } + if (!c.isLast()) c.moveToNext(); + else break; + } + } + if (c != null) c.close(); + + if (l.size() == 0) return null; + + Collections.sort(l); + return l; + } + + @Override + protected void onPostExecute(List<LocalData> l) { + boolean changed = (l != mImages); + LocalData cameraData = null; + if (mImages != null && mImages.size() > 0) { + cameraData = mImages.get(0); + if (cameraData.getType() != ImageData.TYPE_CAMERA_PREVIEW) { + cameraData = null; + } + } + + mImages = l; + if (cameraData != null) { + // camera view exists, so we make sure at least have 1 data in the list. + if (mImages == null) mImages = new ArrayList<LocalData>(); + mImages.add(0, cameraData); + if (mListener != null) { + // Only the camera data is not changed, everything else is changed. + mListener.onDataUpdated(new UpdateReporter() { + @Override + public boolean isDataRemoved(int id) { + return false; + } + + @Override + public boolean isDataUpdated(int id) { + if (id == 0) return false; + return true; + } + }); + } + } else { + // both might be null. + if (changed) mListener.onDataLoaded(); + } + } + } + + private abstract static class LocalData implements + FilmStripView.ImageData, + Comparable<LocalData> { + public long id; + public String title; + public String mimeType; + public long dateTaken; + public long dateModified; + public String path; + // width and height should be adjusted according to orientation. + public int width; + public int height; + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public boolean isActionSupported(int action) { + return false; + } + + private int compare(long v1, long v2) { + return ((v1 > v2) ? 1 : ((v1 < v2) ? -1 : 0)); + } + + @Override + public int compareTo(LocalData d) { + int cmp = compare(d.dateTaken, dateTaken); + if (cmp != 0) return cmp; + cmp = compare(d.dateModified, dateModified); + if (cmp != 0) return cmp; + cmp = d.title.compareTo(title); + if (cmp != 0) return cmp; + return compare(d.id, id); + } + + @Override + public abstract int getType(); + + abstract View getView(Context c, int width, int height, Drawable placeHolder); + } + + private class CameraPreviewData extends LocalData { + CameraPreviewData(int w, int h) { + width = w; + height = h; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public int getType() { + return ImageData.TYPE_CAMERA_PREVIEW; + } + + @Override + View getView(Context c, int width, int height, Drawable placeHolder) { + return mCameraPreviewView; + } + } + + private static class LocalPhotoData extends LocalData { + static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + static final String[] QUERY_PROJECTION = { + ImageColumns._ID, // 0, int + ImageColumns.TITLE, // 1, string + ImageColumns.MIME_TYPE, // 2, string + ImageColumns.DATE_TAKEN, // 3, int + ImageColumns.DATE_MODIFIED, // 4, int + ImageColumns.DATA, // 5, string + ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270 + ImageColumns.WIDTH, // 7, int + ImageColumns.HEIGHT, // 8, int + }; + + private static final int COL_ID = 0; + private static final int COL_TITLE = 1; + private static final int COL_MIME_TYPE = 2; + private static final int COL_DATE_TAKEN = 3; + private static final int COL_DATE_MODIFIED = 4; + private static final int COL_DATA = 5; + private static final int COL_ORIENTATION = 6; + private static final int COL_WIDTH = 7; + private static final int COL_HEIGHT = 8; + + // 32K buffer. + private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; + + // from MediaStore, can only be 0, 90, 180, 270; + public int orientation; + + static LocalPhotoData buildFromCursor(Cursor c) { + LocalPhotoData d = new LocalPhotoData(); + d.id = c.getLong(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.dateTaken = c.getLong(COL_DATE_TAKEN); + d.dateModified = c.getLong(COL_DATE_MODIFIED); + d.path = c.getString(COL_DATA); + d.orientation = c.getInt(COL_ORIENTATION); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + if (d.width <= 0 || d.height <= 0) { + Log.v(TAG, "warning! zero dimension for " + + d.path + ":" + d.width + "x" + d.height); + Dimension dim = decodeDimension(d.path); + if (dim != null) { + d.width = dim.width; + d.height = dim.height; + } else { + Log.v(TAG, "warning! dimension decode failed for " + d.path); + Bitmap b = BitmapFactory.decodeFile(d.path); + if (b == null) return null; + d.width = b.getWidth(); + d.height = b.getHeight(); + } + } + if (d.orientation == 90 || d.orientation == 270) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + @Override + View getView(Context c, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + ImageView v = new ImageView(c); + v.setImageDrawable(placeHolder); + + v.setScaleType(ImageView.ScaleType.FIT_XY); + LoadBitmapTask task = new LoadBitmapTask(v, decodeWidth, decodeHeight); + task.execute(); + return v; + } + + @Override + public String toString() { + return "LocalPhotoData:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",orientation=" + orientation + + ",date=" + new Date(dateTaken); + } + + @Override + public int getType() { + return TYPE_PHOTO; + } + + private static Dimension decodeDimension(String path) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + Bitmap b = BitmapFactory.decodeFile(path, opts); + if (b == null) return null; + Dimension d = new Dimension(); + d.width = opts.outWidth; + d.height = opts.outHeight; + return d; + } + + private static class Dimension { + public int width; + public int height; + } + + private class LoadBitmapTask extends AsyncTask<Void, Void, Bitmap> { + private ImageView mView; + private int mDecodeWidth; + private int mDecodeHeight; + + public LoadBitmapTask(ImageView v, int decodeWidth, int decodeHeight) { + mView = v; + mDecodeWidth = decodeWidth; + mDecodeHeight = decodeHeight; + } + + @Override + protected Bitmap doInBackground(Void... v) { + BitmapFactory.Options opts = null; + Bitmap b; + int sample = 1; + while (mDecodeWidth * sample < width + || mDecodeHeight * sample < height) { + sample *= 2; + } + opts = new BitmapFactory.Options(); + opts.inSampleSize = sample; + opts.inTempStorage = DECODE_TEMP_STORAGE; + if (isCancelled()) return null; + b = BitmapFactory.decodeFile(path, opts); + if (orientation != 0) { + if (isCancelled()) return null; + Matrix m = new Matrix(); + m.setRotate((float) orientation); + b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); + } + return b; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Cannot decode bitmap file:" + path); + return; + } + mView.setScaleType(ImageView.ScaleType.FIT_XY); + mView.setImageBitmap(bitmap); + } + } + } + + private static class LocalVideoData extends LocalData { + static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, " + + VideoColumns._ID + " DESC"; + static final String[] QUERY_PROJECTION = { + VideoColumns._ID, // 0, int + VideoColumns.TITLE, // 1, string + VideoColumns.MIME_TYPE, // 2, string + VideoColumns.DATE_TAKEN, // 3, int + VideoColumns.DATE_MODIFIED, // 4, int + VideoColumns.DATA, // 5, string + VideoColumns.WIDTH, // 6, int + VideoColumns.HEIGHT, // 7, int + VideoColumns.RESOLUTION + }; + + private static final int COL_ID = 0; + private static final int COL_TITLE = 1; + private static final int COL_MIME_TYPE = 2; + private static final int COL_DATE_TAKEN = 3; + private static final int COL_DATE_MODIFIED = 4; + private static final int COL_DATA = 5; + private static final int COL_WIDTH = 6; + private static final int COL_HEIGHT = 7; + + public int resolutionW; + public int resolutionH; + + static LocalVideoData buildFromCursor(Cursor c) { + LocalVideoData d = new LocalVideoData(); + d.id = c.getLong(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.dateTaken = c.getLong(COL_DATE_TAKEN); + d.dateModified = c.getLong(COL_DATE_MODIFIED); + d.path = c.getString(COL_DATA); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(d.path); + String rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (d.width == 0 || d.height == 0) { + d.width = Integer.parseInt(retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); + d.height = Integer.parseInt(retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); + } + retriever.release(); + if (rotation.equals("90") || rotation.equals("270")) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + @Override + View getView(Context c, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + ImageView v = new ImageView(c); + v.setImageDrawable(placeHolder); + + v.setScaleType(ImageView.ScaleType.FIT_XY); + LoadBitmapTask task = new LoadBitmapTask(v); + task.execute(); + return v; + } + + + @Override + public String toString() { + return "LocalVideoData:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",date=" + new Date(dateTaken); + } + + @Override + public int getType() { + return TYPE_PHOTO; + } + + private static Dimension decodeDimension(String path) { + Dimension d = new Dimension(); + return d; + } + + private static class Dimension { + public int width; + public int height; + } + + private class LoadBitmapTask extends AsyncTask<Void, Void, Bitmap> { + private ImageView mView; + + public LoadBitmapTask(ImageView v) { + mView = v; + } + + @Override + protected Bitmap doInBackground(Void... v) { + android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(path); + byte[] data = retriever.getEmbeddedPicture(); + Bitmap bitmap = null; + if (data != null) { + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + } + if (bitmap == null) { + bitmap = (Bitmap) retriever.getFrameAtTime(); + } + retriever.release(); + return bitmap; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Cannot decode video file:" + path); + return; + } + mView.setImageBitmap(bitmap); + } + } + } +} diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java index f4dd823d1..9840b1544 100644 --- a/src/com/android/camera/ui/FaceView.java +++ b/src/com/android/camera/ui/FaceView.java @@ -33,13 +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 { + implements FocusIndicator, Rotatable, + NewPhotoUI.SurfaceTextureSizeChangedListener { private static final String TAG = "CAM FaceView"; private final boolean LOGV = false; // The value for android.hardware.Camera.setDisplayOrientation. @@ -95,6 +97,11 @@ public class FaceView extends View 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; diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java new file mode 100644 index 000000000..f0e2534d3 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java @@ -0,0 +1,107 @@ +/* + * 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.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector. +public class FilmStripGestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripGestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float x, float y, float dx, float dy); + boolean onFling(float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + boolean onDown(float x, float y); + void onScaleEnd(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final Listener mListener; + + public FilmStripGestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + } + + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event) || mScaleDetector.onTouchEvent(event); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll(e2.getX(), e2.getY(), dx, dy); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(velocityX, velocityY); + } + + @Override + public boolean onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + return super.onDown(e); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java new file mode 100644 index 000000000..9aeb96ffd --- /dev/null +++ b/src/com/android/camera/ui/FilmStripView.java @@ -0,0 +1,830 @@ +/* + * 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.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +public class FilmStripView extends ViewGroup { + private static final String TAG = FilmStripView.class.getSimpleName(); + private static final int BUFFER_SIZE = 5; + // Horizontal padding of children. + private static final int H_PADDING = 50; + // Duration to go back to the first. + private static final int DURATION_BACK_ANIM = 500; + private static final int DURATION_SCROLL_TO_FILMSTRIP = 350; + private static final int DURATION_GEOMETRY_ADJUST = 200; + private static final float FILM_STRIP_SCALE = 0.6f; + private static final float MAX_SCALE = 1f; + + private Context mContext; + private FilmStripGestureRecognizer mGestureRecognizer; + private DataAdapter mDataAdapter; + private final Rect mDrawArea = new Rect(); + + private int mCurrentInfo; + private float mScale; + private GeometryAnimator mGeometryAnimator; + private int mCenterPosition = -1; + private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; + + // This is used to resolve the misalignment problem when the device + // orientation is changed. If the current item is in fullscreen, it might + // be shifted because mCenterPosition is not adjusted with the orientation. + // Set this to true when onSizeChanged is called to make sure we adjust + // mCenterPosition accordingly. + private boolean mAnchorPending; + + public interface ImageData { + public static final int TYPE_NONE = 0; + public static final int TYPE_CAMERA_PREVIEW = 1; + public static final int TYPE_PHOTO = 2; + public static final int TYPE_VIDEO = 3; + public static final int TYPE_PHOTOSPHERE = 4; + + // The actions are defined bit-wise so we can use bit operations like + // | and &. + public static final int ACTION_NONE = 0; + public static final int ACTION_PROMOTE = 1; + public static final int ACTION_DEMOTE = 2; + + // SIZE_FULL means disgard the width or height when deciding the view size + // of this ImageData, just use full screen size. + public static final int SIZE_FULL = -2; + + // The values returned by getWidth() and getHeight() will be used for layout. + public int getWidth(); + public int getHeight(); + public int getType(); + public boolean isActionSupported(int action); + } + + public interface DataAdapter { + public interface UpdateReporter { + public boolean isDataRemoved(int id); + public boolean isDataUpdated(int id); + } + + public interface Listener { + // Called when the whole data loading is done. No any assumption + // on previous data. + public void onDataLoaded(); + // Only some of the data is changed. The listener should check + // if any thing needs to be updated. + public void onDataUpdated(UpdateReporter reporter); + public void onDataInserted(int dataID); + public void onDataRemoved(int dataID); + } + + public int getTotalNumber(); + public View getView(Context context, int id); + public ImageData getImageData(int id); + public void suggestSize(int w, int h); + + public void setListener(Listener listener); + } + + // A helper class to tract and calculate the view coordination. + private static class ViewInfo { + private int mDataID; + // the position of the left of the view in the whole filmstrip. + private int mLeftPosition; + private View mView; + private float mOffsetY; + + public ViewInfo(int id, View v) { + v.setPivotX(0f); + v.setPivotY(0f); + mDataID = id; + mView = v; + mLeftPosition = -1; + mOffsetY = 0; + } + + public int getID() { + return mDataID; + } + + public void setLeftPosition(int pos) { + mLeftPosition = pos; + } + + public int getLeftPosition() { + return mLeftPosition; + } + + public float getOffsetY() { + return mOffsetY; + } + + public void setOffsetY(float offset) { + mOffsetY = offset; + } + + public int getCenterX() { + return mLeftPosition + mView.getWidth() / 2; + } + + public View getView() { + return mView; + } + + private void layoutAt(int left, int top) { + mView.layout(left, top, left + mView.getMeasuredWidth(), + top + mView.getMeasuredHeight()); + } + + public void layoutIn(Rect drawArea, int refCenter, float scale) { + // drawArea is where to layout in. + // refCenter is the absolute horizontal position of the center of drawArea. + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale + + mOffsetY); + layoutAt(left, top); + mView.setScaleX(scale); + mView.setScaleY(scale); + } + } + + public FilmStripView(Context context) { + super(context); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mCurrentInfo = (BUFFER_SIZE - 1) / 2; + // This is for positioning camera controller at the same place in + // different orientations. + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + + setWillNotDraw(false); + mContext = context; + mScale = 1.0f; + mGeometryAnimator = new GeometryAnimator(context); + mGestureRecognizer = + new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + } + + public float getScale() { + return mScale; + } + + public boolean isAnchoredTo(int id) { + if (mViewInfo[mCurrentInfo].getID() == id + && mViewInfo[mCurrentInfo].getCenterX() == mCenterPosition) { + return true; + } + return false; + } + + public int getCurrentType() { + if (mDataAdapter == null) return ImageData.TYPE_NONE; + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return ImageData.TYPE_NONE; + return mDataAdapter.getImageData(curr.getID()).getType(); + } + + @Override + public void onDraw(Canvas c) { + if (mGeometryAnimator.hasNewGeometry()) { + layoutChildren(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int boundWidth = MeasureSpec.getSize(widthMeasureSpec); + int boundHeight = MeasureSpec.getSize(heightMeasureSpec); + if (mDataAdapter != null) { + mDataAdapter.suggestSize(boundWidth / 2, boundHeight / 2); + } + + int wMode = View.MeasureSpec.EXACTLY; + int hMode = View.MeasureSpec.EXACTLY; + + for (int i = 0; i < mViewInfo.length; i++) { + ViewInfo info = mViewInfo[i]; + if (mViewInfo[i] == null) continue; + + int imageWidth = mDataAdapter.getImageData(info.getID()).getWidth(); + int imageHeight = mDataAdapter.getImageData(info.getID()).getHeight(); + if (imageWidth == ImageData.SIZE_FULL) imageWidth = boundWidth; + if (imageHeight == ImageData.SIZE_FULL) imageHeight = boundHeight; + + int scaledWidth = boundWidth; + int scaledHeight = boundHeight; + + if (imageWidth * scaledHeight > scaledWidth * imageHeight) { + scaledHeight = imageHeight * scaledWidth / imageWidth; + } else { + scaledWidth = imageWidth * scaledHeight / imageHeight; + } + scaledWidth += H_PADDING * 2; + mViewInfo[i].getView().measure( + View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode) + , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode)); + } + setMeasuredDimension(boundWidth, boundHeight); + } + + private int findTheNearestView(int pointX) { + + int nearest = 0; + // find the first non-null ViewInfo. + for (; nearest < BUFFER_SIZE + && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1); + nearest++); + // no existing available ViewInfo + if (nearest == BUFFER_SIZE) return -1; + int min = Math.abs(pointX - mViewInfo[nearest].getCenterX()); + + for (int infoID = nearest + 1; + infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { + // not measured yet. + if (mViewInfo[infoID].getLeftPosition() == -1) continue; + + int c = mViewInfo[infoID].getCenterX(); + int dist = Math.abs(pointX - c); + if (dist < min) { + min = dist; + nearest = infoID; + } + } + return nearest; + } + + private ViewInfo buildInfoFromData(int dataID) { + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) return null; + v.setPadding(H_PADDING, 0, H_PADDING, 0); + ViewInfo info = new ViewInfo(dataID, v); + addView(info.getView()); + return info; + } + + // We try to keep the one closest to the center of the screen at position mCurrentInfo. + private void stepIfNeeded() { + int nearest = findTheNearestView(mCenterPosition); + // no change made. + if (nearest == -1 || nearest == mCurrentInfo) return; + + int adjust = nearest - mCurrentInfo; + if (adjust > 0) { + for (int k = 0; k < adjust; k++) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = 0; k + adjust < BUFFER_SIZE; k++) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { + mViewInfo[k] = null; + if (mViewInfo[k - 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1); + } + } else { + for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = -1 - adjust; k >= 0; k--) { + mViewInfo[k] = null; + if (mViewInfo[k + 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1); + } + } + } + + // Don't go out of bound. + private void adjustCenterPosition() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return; + + if (curr.getID() == 0 && mCenterPosition < curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); + } + if (curr.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterPosition > curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); + } + } + + private void layoutChildren() { + if (mAnchorPending) { + mCenterPosition = mViewInfo[mCurrentInfo].getCenterX(); + mAnchorPending = false; + } + + if (mGeometryAnimator.hasNewGeometry()) { + mCenterPosition = mGeometryAnimator.getNewPosition(); + mScale = mGeometryAnimator.getNewScale(); + } + + adjustCenterPosition(); + + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition, mScale); + + // images on the left + for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo next = mViewInfo[infoID + 1]; + curr.setLeftPosition( + next.getLeftPosition() - curr.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); + } + } + + // images on the right + for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo prev = mViewInfo[infoID - 1]; + curr.setLeftPosition( + prev.getLeftPosition() + prev.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); + } + } + + stepIfNeeded(); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mViewInfo[mCurrentInfo] == null) return; + + mDrawArea.left = l; + mDrawArea.top = t; + mDrawArea.right = r; + mDrawArea.bottom = b; + + layoutChildren(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (w == oldw && h == oldh) return; + if (mViewInfo[mCurrentInfo] != null && mScale == 1f + && isAnchoredTo(mViewInfo[mCurrentInfo].getID())) { + mAnchorPending = true; + } + } + + public void setDataAdapter(DataAdapter adapter) { + mDataAdapter = adapter; + mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight()); + mDataAdapter.setListener(new DataAdapter.Listener() { + @Override + public void onDataLoaded() { + reload(); + } + + @Override + public void onDataUpdated(DataAdapter.UpdateReporter reporter) { + update(reporter); + } + + @Override + public void onDataInserted(int dataID) { + } + + @Override + public void onDataRemoved(int dataID) { + } + }); + } + + public boolean isInCameraFullscreen() { + return (isAnchoredTo(0) && mScale == 1f + && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (isInCameraFullscreen()) return false; + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mGestureRecognizer.onTouchEvent(ev); + return true; + } + + private void updateViewInfo(int infoID) { + ViewInfo info = mViewInfo[infoID]; + removeView(info.getView()); + mViewInfo[infoID] = buildInfoFromData(info.getID()); + } + + // Some of the data is changed. + private void update(DataAdapter.UpdateReporter reporter) { + // No data yet. + if (mViewInfo[mCurrentInfo] == null) { + reload(); + return; + } + + // Check the current one. + ViewInfo curr = mViewInfo[mCurrentInfo]; + int dataID = curr.getID(); + if (reporter.isDataRemoved(dataID)) { + mCenterPosition = -1; + reload(); + return; + } + if (reporter.isDataUpdated(dataID)) { + updateViewInfo(mCurrentInfo); + } + + // Check left + for (int i = mCurrentInfo - 1; i >= 0; i--) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo next = mViewInfo[i + 1]; + if (next != null) mViewInfo[i] = buildInfoFromData(next.getID() - 1); + } + } + + // Check right + for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo prev = mViewInfo[i - 1]; + if (prev != null) mViewInfo[i] = buildInfoFromData(prev.getID() + 1); + } + } + } + + // The whole data might be totally different. Flush all and load from the start. + private void reload() { + removeAllViews(); + int dataNumber = mDataAdapter.getTotalNumber(); + if (dataNumber == 0) return; + + int currentData = 0; + int currentLeft = 0; + mViewInfo[mCurrentInfo] = buildInfoFromData(currentData); + mViewInfo[mCurrentInfo].setLeftPosition(currentLeft); + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW + && currentLeft == 0) { + // we are in camera mode by default. + mGeometryAnimator.lockPosition(currentLeft); + } + for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { + int infoID = mCurrentInfo + i; + if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1); + } + infoID = mCurrentInfo - i; + if (infoID >= 0 && mViewInfo[infoID + 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1); + } + } + layoutChildren(); + } + + // GeometryAnimator controls all the geometry animations. It passively + // tells the geometry information on demand. + private class GeometryAnimator implements + ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private ValueAnimator mScaleAnimator; + private boolean mHasNewScale; + private float mNewScale; + + private Scroller mScroller; + private boolean mHasNewPosition; + private DecelerateInterpolator mDecelerateInterpolator; + + private boolean mCanStopScroll; + private boolean mCanStopScale; + + private boolean mIsPositionLocked; + private int mLockedPosition; + + private Runnable mPostAction; + + GeometryAnimator(Context context) { + mScroller = new Scroller(context); + mHasNewPosition = false; + mScaleAnimator = new ValueAnimator(); + mScaleAnimator.addUpdateListener(GeometryAnimator.this); + mScaleAnimator.addListener(GeometryAnimator.this); + mDecelerateInterpolator = new DecelerateInterpolator(); + mCanStopScroll = true; + mCanStopScale = true; + mHasNewScale = false; + } + + boolean hasNewGeometry() { + mHasNewPosition = mScroller.computeScrollOffset(); + if (!mHasNewPosition) { + mCanStopScroll = true; + } + // If the position is locked, then we always return true to force + // the position value to use the locked value. + return (mHasNewPosition || mHasNewScale || mIsPositionLocked); + } + + // Always call hasNewGeometry() before getting the new scale value. + float getNewScale() { + if (!mHasNewScale) return mScale; + mHasNewScale = false; + return mNewScale; + } + + // Always call hasNewGeometry() before getting the new position value. + int getNewPosition() { + if (mIsPositionLocked) return mLockedPosition; + if (!mHasNewPosition) return mCenterPosition; + return mScroller.getCurrX(); + } + + void lockPosition(int pos) { + mIsPositionLocked = true; + mLockedPosition = pos; + } + + void unlockPosition() { + if (mIsPositionLocked) { + // only when the position is previously locked we set the current + // position to make it consistent. + mCenterPosition = mLockedPosition; + mIsPositionLocked = false; + } + } + + void fling(int velocityX, int minX, int maxX) { + if (!stopScroll() || mIsPositionLocked) return; + mScroller.fling(mCenterPosition, 0, velocityX, 0, minX, maxX, 0, 0); + } + + boolean stopScroll() { + if (!mCanStopScroll) return false; + mScroller.forceFinished(true); + mHasNewPosition = false; + return true; + } + + boolean stopScale() { + if (!mCanStopScale) return false; + mScaleAnimator.cancel(); + mHasNewScale = false; + return true; + } + + void stop() { + stopScroll(); + stopScale(); + } + + void scrollTo(int position, int duration, boolean interruptible) { + if (!stopScroll() || mIsPositionLocked) return; + mCanStopScroll = interruptible; + stopScroll(); + mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, + 0, duration); + } + + void scrollTo(int position, int duration) { + scrollTo(position, duration, true); + } + + void scaleTo(float scale, int duration, boolean interruptible) { + if (!stopScale()) return; + mCanStopScale = interruptible; + mScaleAnimator.setDuration(duration); + mScaleAnimator.setFloatValues(mScale, scale); + mScaleAnimator.setInterpolator(mDecelerateInterpolator); + mScaleAnimator.start(); + mHasNewScale = true; + } + + void scaleTo(float scale, int duration) { + scaleTo(scale, duration, true); + } + + void setPostAction(Runnable act) { + mPostAction = act; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHasNewScale = true; + mNewScale = (Float) animation.getAnimatedValue(); + layoutChildren(); + } + + @Override + public void onAnimationStart(Animator anim) { + } + + @Override + public void onAnimationEnd(Animator anim) { + if (mPostAction != null) { + mPostAction.run(); + mPostAction = null; + } + mCanStopScale = true; + } + + @Override + public void onAnimationCancel(Animator anim) { + mPostAction = null; + } + + @Override + public void onAnimationRepeat(Animator anim) { + } + } + + private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener { + // Indicating the current trend of scaling is up (>1) or down (<1). + private float mScaleTrend; + + @Override + public boolean onSingleTapUp(float x, float y) { + return false; + } + + @Override + public boolean onDoubleTap(float x, float y) { + return false; + } + + @Override + public boolean onDown(float x, float y) { + mGeometryAnimator.stop(); + return true; + } + + @Override + public boolean onScroll(float x, float y, float dx, float dy) { + int deltaX = (int) (dx / mScale); + if (deltaX > 0 && isInCameraFullscreen()) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } + + mCenterPosition += deltaX; + + // Vertical part. Promote or demote. + int scaledDeltaY = (int) (dy / mScale); + + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) continue; + Rect hitRect = new Rect(); + View v = mViewInfo[i].getView(); + v.getHitRect(hitRect); + if (hitRect.contains((int) x, (int) y)) { + ImageData data = mDataAdapter.getImageData(mViewInfo[i].getID()); + if ((data.isActionSupported(ImageData.ACTION_DEMOTE) && dy > 0) + || (data.isActionSupported(ImageData.ACTION_PROMOTE) && dy < 0)) { + mViewInfo[i].setOffsetY(mViewInfo[i].getOffsetY() - dy); + } + break; + } + } + + layoutChildren(); + return true; + } + + @Override + public boolean onFling(float velocityX, float velocityY) { + float scaledVelocityX = velocityX / mScale; + if (isInCameraFullscreen() && scaledVelocityX < 0) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } + ViewInfo info = mViewInfo[mCurrentInfo]; + int w = getWidth(); + if (info == null) return true; + mGeometryAnimator.fling((int) -scaledVelocityX, + // estimation of possible length on the left + info.getLeftPosition() - info.getID() * w * 2, + // estimation of possible length on the right + info.getLeftPosition() + + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2); + layoutChildren(); + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (isInCameraFullscreen()) return false; + mScaleTrend = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (isInCameraFullscreen()) return false; + + mScaleTrend = mScaleTrend * 0.5f + scale * 0.5f; + mScale *= scale; + if (mScale <= FILM_STRIP_SCALE) mScale = FILM_STRIP_SCALE; + if (mScale >= MAX_SCALE) mScale = MAX_SCALE; + layoutChildren(); + return true; + } + + @Override + public void onScaleEnd() { + if (mScaleTrend >= 1f) { + if (mScale != 1f) { + mGeometryAnimator.scaleTo(1f, DURATION_GEOMETRY_ADJUST, false); + } + + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) { + if (isAnchoredTo(0)) { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } else { + mGeometryAnimator.scrollTo( + mViewInfo[mCurrentInfo].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mLockPositionRunnable); + } + } + } else { + // Scale down to film strip mode. + if (mScale == FILM_STRIP_SCALE) { + mGeometryAnimator.unlockPosition(); + return; + } + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mUnlockPositionRunnable); + } + } + + private Runnable mLockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } + }; + + private Runnable mUnlockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.unlockPosition(); + } + }; + } +} diff --git a/src/com/android/camera/ui/NewCameraRootView.java b/src/com/android/camera/ui/NewCameraRootView.java new file mode 100644 index 000000000..a507b147c --- /dev/null +++ b/src/com/android/camera/ui/NewCameraRootView.java @@ -0,0 +1,91 @@ +/* + * 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_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); + } + } +} diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java index a4f5807ae..9adb4e7a8 100644 --- a/src/com/android/gallery3d/app/CommonControllerOverlay.java +++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java @@ -66,6 +66,10 @@ public abstract class CommonControllerOverlay extends FrameLayout implements protected boolean mCanReplay = true; + public void setSeekable(boolean canSeek) { + mTimeBar.setSeekable(canSeek); + } + public CommonControllerOverlay(Context context) { super(context); diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java index 00e4cd63b..ce9183483 100644 --- a/src/com/android/gallery3d/app/MoviePlayer.java +++ b/src/com/android/gallery3d/app/MoviePlayer.java @@ -25,7 +25,6 @@ import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Color; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; @@ -135,6 +134,17 @@ public class MoviePlayer implements return true; } }); + mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer player) { + if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) { + mController.setSeekable(false); + } else { + mController.setSeekable(true); + } + setProgress(); + } + }); // The SurfaceView is transparent before drawing the first frame. // This makes the UI flashing when open a video. (black -> old screen diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java index 402dfcfab..246346a56 100644 --- a/src/com/android/gallery3d/app/TimeBar.java +++ b/src/com/android/gallery3d/app/TimeBar.java @@ -259,4 +259,8 @@ public class TimeBar extends View { } } + public void setSeekable(boolean canSeek) { + mShowScrubber = canSeek; + } + } diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java index d26a58a34..a94626123 100644 --- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -69,6 +69,8 @@ import com.android.gallery3d.filtershow.provider.SharedImageProvider; import com.android.gallery3d.filtershow.state.StateAdapter; import com.android.gallery3d.filtershow.tools.BitmapTask; import com.android.gallery3d.filtershow.tools.SaveCopyTask; +import com.android.gallery3d.filtershow.tools.XmpPresets; +import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults; import com.android.gallery3d.filtershow.ui.FramedTextButton; import com.android.gallery3d.filtershow.ui.Spline; import com.android.gallery3d.util.GalleryUtils; @@ -119,6 +121,9 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL private LoadBitmapTask mLoadBitmapTask; private boolean mLoading = true; + private Uri mOriginalImageUri = null; + private ImagePreset mOriginalPreset = null; + private CategoryAdapter mCategoryLooksAdapter = null; private CategoryAdapter mCategoryBordersAdapter = null; private CategoryAdapter mCategoryGeometryAdapter = null; @@ -147,6 +152,7 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL setDefaultPreset(); + extractXMPData(); processIntent(); } @@ -264,9 +270,12 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL } mAction = intent.getAction(); - - if (intent.getData() != null) { - startLoadBitmap(intent.getData()); + Uri srcUri = intent.getData(); + if (mOriginalImageUri != null) { + srcUri = mOriginalImageUri; + } + if (srcUri != null) { + startLoadBitmap(srcUri); } else { pickImage(); } @@ -343,8 +352,9 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL for (int i = 0; i < borders.size(); i++) { FilterRepresentation filter = borders.elementAt(i); + filter.setScrName(getString(R.string.borders)); if (i == 0) { - filter.setName(getString(R.string.none)); + filter.setScrName(getString(R.string.none)); } } @@ -495,6 +505,11 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL mCategoryFiltersAdapter.imageLoaded(); mLoadBitmapTask = null; + if (mOriginalPreset != null) { + MasterImage.getImage().setPreset(mOriginalPreset, true); + mOriginalPreset = null; + } + if (mAction == TINY_PLANET_ACTION) { showRepresentation(mCategoryFiltersAdapter.getTinyPlanet()); } @@ -1027,4 +1042,13 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL System.loadLibrary("jni_filtershow_filters"); } + private void extractXMPData() { + XMresults res = XmpPresets.extractXMPData( + getBaseContext(), mMasterImage, getIntent().getData()); + if (res == null) + return; + + mOriginalImageUri = res.originalimage; + mOriginalPreset = res.preset; + } } diff --git a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java index 8760c4a09..1ea40f202 100644 --- a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java +++ b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java @@ -29,8 +29,9 @@ import com.android.gallery3d.filtershow.imageshow.GeometryMetadata; import com.android.gallery3d.filtershow.imageshow.MasterImage; import com.android.gallery3d.filtershow.presets.FilterEnvironment; import com.android.gallery3d.filtershow.presets.ImagePreset; +import com.android.gallery3d.filtershow.presets.PipelineInterface; -public class CachingPipeline { +public class CachingPipeline implements PipelineInterface { private static final String LOGTAG = "CachingPipeline"; private boolean DEBUG = false; @@ -65,22 +66,10 @@ public class CachingPipeline { mName = name; } - public static synchronized Resources getResources() { - return sResources; - } - - public static synchronized void setResources(Resources resources) { - sResources = resources; - } - public static synchronized RenderScript getRenderScriptContext() { return sRS; } - public static synchronized void setRenderScriptContext(RenderScript RS) { - sRS = RS; - } - public static synchronized void createRenderscriptContext(Activity context) { if (sRS != null) { Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext"); @@ -128,6 +117,10 @@ public class CachingPipeline { } } + public Resources getResources() { + return sRS.getApplicationContext().getResources(); + } + private synchronized void destroyPixelAllocations() { if (DEBUG) { Log.v(LOGTAG, "destroyPixelAllocations in " + getName()); @@ -167,14 +160,14 @@ public class CachingPipeline { } private void setupEnvironment(ImagePreset preset, boolean highResPreview) { - mEnvironment.setCachingPipeline(this); + mEnvironment.setPipeline(this); mEnvironment.setFiltersManager(mFiltersManager); if (highResPreview) { mEnvironment.setScaleFactor(mHighResPreviewScaleFactor); } else { mEnvironment.setScaleFactor(mPreviewScaleFactor); } - mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW); + mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW); mEnvironment.setImagePreset(preset); mEnvironment.setStop(false); } @@ -293,11 +286,11 @@ public class CachingPipeline { || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) { if (request.getType() == RenderingRequest.ICON_RENDERING) { - mEnvironment.setQuality(ImagePreset.QUALITY_ICON); + mEnvironment.setQuality(FilterEnvironment.QUALITY_ICON); } else if (request.getType() == RenderingRequest.STYLE_ICON_RENDERING) { mEnvironment.setQuality(ImagePreset.STYLE_ICON); } else { - mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW); + mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW); } Bitmap bmp = preset.apply(bitmap, mEnvironment); @@ -317,8 +310,11 @@ public class CachingPipeline { setupEnvironment(preset, false); mFiltersManager.freeFilterResources(preset); preset.applyFilters(-1, -1, in, out, mEnvironment); - // TODO: we should render the border onto a different bitmap instead - preset.applyBorder(in, out, mEnvironment); + boolean copyOut = false; + if (preset.nbFilters() > 0) { + copyOut = true; + } + preset.applyBorder(in, out, copyOut, mEnvironment); } } @@ -328,7 +324,7 @@ public class CachingPipeline { return bitmap; } setupEnvironment(preset, false); - mEnvironment.setQuality(ImagePreset.QUALITY_FINAL); + mEnvironment.setQuality(FilterEnvironment.QUALITY_FINAL); mEnvironment.setScaleFactor(1.0f); mFiltersManager.freeFilterResources(preset); bitmap = preset.applyGeometry(bitmap, mEnvironment); @@ -345,7 +341,7 @@ public class CachingPipeline { } mGeometry.useRepresentation(preset.getGeometry()); return mGeometry.apply(bitmap, mPreviewScaleFactor, - ImagePreset.QUALITY_PREVIEW); + FilterEnvironment.QUALITY_PREVIEW); } public synchronized void compute(TripleBufferBitmap buffer, ImagePreset preset, int type) { @@ -458,4 +454,7 @@ public class CachingPipeline { return mName; } + public RenderScript getRSContext() { + return CachingPipeline.getRenderScriptContext(); + } } diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java index 072edd72a..25169c2d8 100644 --- a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java +++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java @@ -109,5 +109,4 @@ public class BasicParameterStyle implements ParameterStyles { public void setFilterView(FilterView editor) { mEditor = editor; } - } diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java index df5b6ae73..9d8278d52 100644 --- a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java +++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java @@ -84,5 +84,4 @@ public class BasicSlider implements Control { mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum()); mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum()); } - } diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java new file mode 100644 index 000000000..e18d3104f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java @@ -0,0 +1,101 @@ +/* + * 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.gallery3d.filtershow.data; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class FilterStackDBHelper extends SQLiteOpenHelper { + + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "filterstacks.db"; + private static final String SQL_CREATE_TABLE = "CREATE TABLE "; + + public static interface FilterStack { + /** The row uid */ + public static final String _ID = "_id"; + /** The table name */ + public static final String TABLE = "filterstack"; + /** The stack name */ + public static final String STACK_ID = "stack_id"; + /** A serialized stack of filters. */ + public static final String FILTER_STACK= "stack"; + } + + private static final String[][] CREATE_FILTER_STACK = { + { FilterStack._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" }, + { FilterStack.STACK_ID, "TEXT" }, + { FilterStack.FILTER_STACK, "BLOB" }, + }; + + public FilterStackDBHelper(Context context, String name, int version) { + super(context, name, null, version); + } + + public FilterStackDBHelper(Context context, String name) { + this(context, name, DATABASE_VERSION); + } + + public FilterStackDBHelper(Context context) { + this(context, DATABASE_NAME); + } + + @Override + public void onCreate(SQLiteDatabase db) { + createTable(db, FilterStack.TABLE, CREATE_FILTER_STACK); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + dropTable(db, FilterStack.TABLE); + onCreate(db); + } + + protected static void createTable(SQLiteDatabase db, String table, String[][] columns) { + StringBuilder create = new StringBuilder(SQL_CREATE_TABLE); + create.append(table).append('('); + boolean first = true; + for (String[] column : columns) { + if (!first) { + create.append(','); + } + first = false; + for (String val : column) { + create.append(val).append(' '); + } + } + create.append(')'); + db.beginTransaction(); + try { + db.execSQL(create.toString()); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + protected static void dropTable(SQLiteDatabase db, String table) { + db.beginTransaction(); + try { + db.execSQL("drop table if exists " + table); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackSource.java b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java new file mode 100644 index 000000000..4e343777d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java @@ -0,0 +1,143 @@ +/* + * 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.gallery3d.filtershow.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +import com.android.gallery3d.filtershow.data.FilterStackDBHelper.FilterStack; + +import java.util.ArrayList; +import java.util.List; + +public class FilterStackSource { + private static final String LOGTAG = "FilterStackSource"; + + private SQLiteDatabase database = null;; + private final FilterStackDBHelper dbHelper; + + public FilterStackSource(Context context) { + dbHelper = new FilterStackDBHelper(context); + } + + public void open() { + try { + database = dbHelper.getWritableDatabase(); + } catch (SQLiteException e) { + Log.w(LOGTAG, "could not open database", e); + } + } + + public void close() { + database = null; + dbHelper.close(); + } + + public boolean insertStack(String stackName, byte[] stackBlob) { + boolean ret = true; + ContentValues val = new ContentValues(); + val.put(FilterStack.STACK_ID, stackName); + val.put(FilterStack.FILTER_STACK, stackBlob); + database.beginTransaction(); + try { + ret = (-1 != database.insert(FilterStack.TABLE, null, val)); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + return ret; + } + + public boolean removeStack(String stackName) { + boolean ret = true; + database.beginTransaction(); + try { + ret = (0 != database.delete(FilterStack.TABLE, FilterStack.STACK_ID + " = ?", + new String[] { stackName})); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + return ret; + } + + public void removeAllStacks() { + database.beginTransaction(); + try { + database.delete(FilterStack.TABLE, null, null); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + public byte[] getStack(String stackName) { + byte[] ret = null; + Cursor c = null; + database.beginTransaction(); + try { + c = database.query(FilterStack.TABLE, + new String[] { FilterStack.FILTER_STACK }, + FilterStack.STACK_ID + " = ?", + new String[] { stackName }, null, null, null, null); + if (c != null && c.moveToFirst() && !c.isNull(0)) { + ret = c.getBlob(0); + } + database.setTransactionSuccessful(); + } finally { + if (c != null) { + c.close(); + } + database.endTransaction(); + } + return ret; + } + + public List<Pair<String, byte[]>> getAllStacks() { + List<Pair<String, byte[]>> ret = new ArrayList<Pair<String, byte[]>>(); + Cursor c = null; + database.beginTransaction(); + try { + c = database.query(FilterStack.TABLE, + new String[] { FilterStack.STACK_ID, FilterStack.FILTER_STACK }, + null, null, null, null, null, null); + if (c != null) { + boolean loopCheck = c.moveToFirst(); + while (loopCheck) { + String name = (c.isNull(0)) ? null : c.getString(0); + byte[] b = (c.isNull(1)) ? null : c.getBlob(1); + ret.add(new Pair<String, byte[]>(name, b)); + loopCheck = c.moveToNext(); + } + } + database.setTransactionSuccessful(); + } finally { + if (c != null) { + c.close(); + } + database.endTransaction(); + } + if (ret.size() <= 0) { + return null; + } + return ret; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java index 9927a0a5e..42edbbbe9 100644 --- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java +++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java @@ -17,6 +17,8 @@ package com.android.gallery3d.filtershow.filters; import android.content.Context; import android.content.res.Resources; +import android.util.Log; + import com.android.gallery3d.R; import com.android.gallery3d.filtershow.presets.ImagePreset; @@ -24,11 +26,14 @@ import com.android.gallery3d.filtershow.presets.ImagePreset; import java.util.HashMap; import java.util.Vector; -public abstract class BaseFiltersManager { +public abstract class BaseFiltersManager implements FiltersManagerInterface { protected HashMap<Class, ImageFilter> mFilters = null; + protected HashMap<String, FilterRepresentation> mRepresentationLookup = null; + private static final String LOGTAG = "BaseFiltersManager"; protected void init() { mFilters = new HashMap<Class, ImageFilter>(); + mRepresentationLookup = new HashMap<String, FilterRepresentation>(); Vector<Class> filters = new Vector<Class>(); addFilterClasses(filters); for (Class filterClass : filters) { @@ -36,6 +41,12 @@ public abstract class BaseFiltersManager { Object filterInstance = filterClass.newInstance(); if (filterInstance instanceof ImageFilter) { mFilters.put(filterClass, (ImageFilter) filterInstance); + + FilterRepresentation rep = + ((ImageFilter) filterInstance).getDefaultRepresentation(); + if (rep != null) { + addRepresentation(rep); + } } } catch (InstantiationException e) { e.printStackTrace(); @@ -45,6 +56,20 @@ public abstract class BaseFiltersManager { } } + public void addRepresentation(FilterRepresentation rep) { + mRepresentationLookup.put(rep.getSerializationName(), rep); + } + + public FilterRepresentation createFilterFromName(String name) { + try { + return mRepresentationLookup.get(name).clone(); + } catch (Exception e) { + Log.v(LOGTAG, "unable to generate a filter representation for \"" + name + "\""); + e.printStackTrace(); + } + return null; + } + public ImageFilter getFilter(Class c) { return mFilters.get(c); } @@ -53,10 +78,6 @@ public abstract class BaseFiltersManager { return mFilters.get(representation.getFilterClass()); } - public void addFilter(Class filterClass, ImageFilter filter) { - mFilters.put(filterClass, filter); - } - public FilterRepresentation getRepresentation(Class c) { ImageFilter filter = mFilters.get(c); if (filter != null) { @@ -89,7 +110,7 @@ public abstract class BaseFiltersManager { protected void addFilterClasses(Vector<Class> filters) { filters.add(ImageFilterTinyPlanet.class); - //filters.add(ImageFilterRedEye.class); + filters.add(ImageFilterRedEye.class); filters.add(ImageFilterWBalance.class); filters.add(ImageFilterExposure.class); filters.add(ImageFilterVignette.class); @@ -168,8 +189,8 @@ public abstract class BaseFiltersManager { } public void addTools(Vector<FilterRepresentation> representations) { - //representations.add(getRepresentation(ImageFilterRedEye.class)); - // representations.add(getRepresentation(ImageFilterDraw.class)); + representations.add(getRepresentation(ImageFilterRedEye.class)); + representations.add(getRepresentation(ImageFilterDraw.class)); } public void setFilterResources(Resources resources) { diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java index 28ccf7d8f..d45ed56f2 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java @@ -29,6 +29,8 @@ public class FilterBasicRepresentation extends FilterRepresentation implements P private int mMaximum; private int mDefaultValue; private int mPreviewValue; + public static final String SERIAL_NAME = "Name"; + public static final String SERIAL_VALUE = "Value"; public FilterBasicRepresentation(String name, int minimum, int value, int maximum) { super(name); @@ -166,4 +168,23 @@ public class FilterBasicRepresentation extends FilterRepresentation implements P public void copyFrom(Parameter src) { useParametersFrom((FilterBasicRepresentation) src); } + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + {SERIAL_NAME , getName() }, + {SERIAL_VALUE , Integer.toString(mValue)}}; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + if (SERIAL_VALUE.equals(rep[i][0])) { + mValue = Integer.parseInt(rep[i][1]); + break; + } + } + } } diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java index cbcae4b37..a32068aeb 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java @@ -15,6 +15,7 @@ public class FilterCurvesRepresentation extends FilterRepresentation { public FilterCurvesRepresentation() { super("Curves"); + setSerializationName("CURVES"); setFilterClass(ImageFilterCurves.class); setTextId(R.string.curvesRGB); setButtonId(R.id.curvesButtonRGB); diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java index dc59b0cfc..9b144b9e9 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java @@ -49,6 +49,7 @@ public class FilterDrawRepresentation extends FilterRepresentation { public FilterDrawRepresentation() { super("Draw"); + setSerializationName("DRAW"); setFilterClass(ImageFilterDraw.class); setPriority(FilterRepresentation.TYPE_VIGNETTE); setTextId(R.string.imageDraw); diff --git a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java index 6e2e7ea16..1ceffb4a2 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java @@ -21,7 +21,8 @@ import com.android.gallery3d.app.Log; import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; public class FilterFxRepresentation extends FilterRepresentation { - private static final String LOGTAG = "FilterFxRepresentation"; + private static final String SERIALIZATION_NAME = "LUT3D"; + private static final String LOGTAG = "FilterFxRepresentation"; // TODO: When implementing serialization, we should find a unique way of // specifying bitmaps / names (the resource IDs being random) private int mBitmapResource = 0; @@ -29,6 +30,8 @@ public class FilterFxRepresentation extends FilterRepresentation { public FilterFxRepresentation(String name, int bitmapResource, int nameResource) { super(name); + setSerializationName(SERIALIZATION_NAME); + mBitmapResource = bitmapResource; mNameResource = nameResource; setFilterClass(ImageFilterFx.class); diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java index 3f823ea1e..8a878415c 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java @@ -28,6 +28,7 @@ public class FilterRedEyeRepresentation extends FilterPointRepresentation { public FilterRedEyeRepresentation() { super("RedEye",R.string.redeye,EditorRedEye.ID); + setSerializationName("REDEYE"); setFilterClass(ImageFilterRedEye.class); setOverlayId(R.drawable.photoeditor_effect_redeye); setOverlayOnly(true); diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java index 5bb0e5733..37d66f829 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java @@ -16,7 +16,7 @@ package com.android.gallery3d.filtershow.filters; -import com.android.gallery3d.app.Log; +import android.util.Log; import com.android.gallery3d.filtershow.editors.BasicEditor; public class FilterRepresentation implements Cloneable { @@ -34,7 +34,7 @@ public class FilterRepresentation implements Cloneable { private boolean mShowEditingControls = true; private boolean mShowParameterValue = true; private boolean mShowUtilityPanel = true; - + private String mSerializationName; public static final byte TYPE_BORDER = 1; public static final byte TYPE_FX = 2; public static final byte TYPE_WBALANCE = 3; @@ -63,6 +63,8 @@ public class FilterRepresentation implements Cloneable { representation.setShowEditingControls(showEditingControls()); representation.setShowParameterValue(showParameterValue()); representation.setShowUtilityPanel(showUtilityPanel()); + representation.mSerializationName = mSerializationName; + representation.mTempRepresentation = mTempRepresentation != null ? mTempRepresentation.clone() : null; if (DEBUG) { @@ -96,6 +98,10 @@ public class FilterRepresentation implements Cloneable { return mName; } + public void setScrName(String name) { + mName = name; + } + public void setName(String name) { mName = name; } @@ -104,6 +110,14 @@ public class FilterRepresentation implements Cloneable { return mName; } + public void setSerializationName(String sname) { + mSerializationName = sname; + } + + public String getSerializationName() { + return mSerializationName; + } + public void setPriority(int priority) { mPriority = priority; } @@ -241,4 +255,17 @@ public class FilterRepresentation implements Cloneable { return ""; } + public String[][] serializeRepresentation() { + String[][] ret = { { "Name" , getName() }}; + return ret; + } + + public void deSerializeRepresentation(String[][] rep) { + for (int i = 0; i < rep.length; i++) { + if ("Name".equals(rep[i][0])) { + mName = rep[i][0]; + break; + } + } + } } diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java index ac5e04601..48c8b380e 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java @@ -20,11 +20,14 @@ import com.android.gallery3d.R; import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation { + private static final String SERIALIZATION_NAME = "TINYPLANET"; private static final String LOGTAG = "FilterTinyPlanetRepresentation"; + private static final String SERIAL_ANGLE = "Angle"; private float mAngle = 0; public FilterTinyPlanetRepresentation() { super("TinyPlanet", 0, 50, 100); + setSerializationName(SERIALIZATION_NAME); setShowParameterValue(true); setFilterClass(ImageFilterTinyPlanet.class); setPriority(FilterRepresentation.TYPE_TINYPLANET); @@ -71,4 +74,25 @@ public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation { // TinyPlanet always has an effect return false; } + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + {SERIAL_NAME , getName() }, + {SERIAL_VALUE , Integer.toString(getValue())}, + {SERIAL_ANGLE , Float.toString(mAngle)}}; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + if (SERIAL_VALUE.equals(rep[i][0])) { + setValue(Integer.parseInt(rep[i][1])); + } else if (SERIAL_ANGLE.equals(rep[i][0])) { + setAngle(Float.parseFloat(rep[i][1])); + } + } + } } diff --git a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java index eef54ef58..9827088ff 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java @@ -29,6 +29,7 @@ public class FilterVignetteRepresentation extends FilterBasicRepresentation impl public FilterVignetteRepresentation() { super("Vignette", -100, 50, 100); + setSerializationName("VIGNETTE"); setShowParameterValue(true); setPriority(FilterRepresentation.TYPE_VIGNETTE); setTextId(R.string.vignette); @@ -111,4 +112,44 @@ public class FilterVignetteRepresentation extends FilterBasicRepresentation impl public boolean isNil() { return getValue() == 0; } + + private static final String[] sParams = { + "Name", "value", "mCenterX", "mCenterY", "mRadiusX", + "mRadiusY" + }; + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + { sParams[0], getName() }, + { sParams[1], Integer.toString(getValue()) }, + { sParams[2], Float.toString(mCenterX) }, + { sParams[3], Float.toString(mCenterY) }, + { sParams[4], Float.toString(mRadiusX) }, + { sParams[5], Float.toString(mRadiusY) } + }; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + String key = rep[i][0]; + String value = rep[i][1]; + if (sParams[0].equals(key)) { + setName(value); + } else if (sParams[1].equals(key)) { + setValue(Integer.parseInt(value)); + } else if (sParams[2].equals(key)) { + mCenterX = Float.parseFloat(value); + } else if (sParams[3].equals(key)) { + mCenterY = Float.parseFloat(value); + } else if (sParams[4].equals(key)) { + mRadiusX = Float.parseFloat(value); + } else if (sParams[5].equals(key)) { + mRadiusY = Float.parseFloat(value); + } + } + } } diff --git a/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java new file mode 100644 index 000000000..710128f99 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java @@ -0,0 +1,21 @@ +/* + * 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.gallery3d.filtershow.filters; + +public interface FiltersManagerInterface { + ImageFilter getFilterForRepresentation(FilterRepresentation representation); +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java index 96ab84f9d..b80fc7f15 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java @@ -16,12 +16,12 @@ package com.android.gallery3d.filtershow.filters; +import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Matrix; import android.support.v8.renderscript.Allocation; import android.widget.Toast; -import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.imageshow.GeometryMetadata; import com.android.gallery3d.filtershow.presets.FilterEnvironment; import com.android.gallery3d.filtershow.presets.ImagePreset; @@ -35,9 +35,9 @@ public abstract class ImageFilter implements Cloneable { // TODO: Temporary, for dogfood note memory issues with toasts for better // feedback. Remove this when filters actually work in low memory // situations. - private static FilterShowActivity sActivity = null; + private static Activity sActivity = null; - public static void setActivityForMemoryToasts(FilterShowActivity activity) { + public static void setActivityForMemoryToasts(Activity activity) { sActivity = activity; } @@ -76,10 +76,6 @@ public abstract class ImageFilter implements Cloneable { return bitmap; } - public ImagePreset getImagePreset() { - return getEnvironment().getImagePreset(); - } - public abstract void useRepresentation(FilterRepresentation representation); native protected void nativeApplyGradientFilter(Bitmap bitmap, int w, int h, @@ -90,10 +86,11 @@ public abstract class ImageFilter implements Cloneable { } protected Matrix getOriginalToScreenMatrix(int w, int h) { - GeometryMetadata geo = getImagePreset().mGeoData; + ImagePreset preset = getEnvironment().getImagePreset(); + GeometryMetadata geo = getEnvironment().getImagePreset().mGeoData; Matrix originalToScreen = geo.getOriginalToScreen(true, - getImagePreset().getImageLoader().getOriginalBounds().width(), - getImagePreset().getImageLoader().getOriginalBounds().height(), + preset.getImageLoader().getOriginalBounds().width(), + preset.getImageLoader().getOriginalBounds().height(), w, h); return originalToScreen; } diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java index a4626cdb2..64c48dffa 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java @@ -23,6 +23,7 @@ import android.graphics.Color; public class ImageFilterBwFilter extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "BWFILTER"; public ImageFilterBwFilter() { mName = "BW Filter"; @@ -31,6 +32,8 @@ public class ImageFilterBwFilter extends SimpleImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("BW Filter"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterBwFilter.class); representation.setMaximum(180); representation.setMinimum(-180); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java index 2097f0d6e..c8b41c248 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java @@ -21,6 +21,7 @@ import com.android.gallery3d.R; import android.graphics.Bitmap; public class ImageFilterContrast extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "CONTRAST"; public ImageFilterContrast() { mName = "Contrast"; @@ -30,6 +31,8 @@ public class ImageFilterContrast extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Contrast"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterContrast.class); representation.setTextId(R.string.contrast); representation.setButtonId(R.id.contrastButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java index 0b02fc4f6..ea2ff351d 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java @@ -24,6 +24,7 @@ import com.android.gallery3d.R; import com.android.gallery3d.filtershow.cache.ImageLoader; public class ImageFilterDownsample extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "DOWNSAMPLE"; private static final int ICON_DOWNSAMPLE_FRACTION = 8; private ImageLoader mImageLoader; @@ -35,6 +36,8 @@ public class ImageFilterDownsample extends SimpleImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Downsample"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterDownsample.class); representation.setMaximum(100); representation.setMinimum(1); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java index 1fd9071f7..812ab02f0 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java @@ -31,6 +31,7 @@ import android.graphics.PorterDuffColorFilter; import com.android.gallery3d.R; import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData; import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.presets.FilterEnvironment; import com.android.gallery3d.filtershow.presets.ImagePreset; import java.util.Vector; @@ -204,7 +205,7 @@ public class ImageFilterDraw extends ImageFilter { public void drawData(Canvas canvas, Matrix originalRotateToScreen, int quality) { Paint paint = new Paint(); - if (quality == ImagePreset.QUALITY_FINAL) { + if (quality == FilterEnvironment.QUALITY_FINAL) { paint.setAntiAlias(true); } paint.setStyle(Style.STROKE); @@ -214,7 +215,7 @@ public class ImageFilterDraw extends ImageFilter { if (mParameters.getDrawing().isEmpty() && mParameters.getCurrentDrawing() == null) { return; } - if (quality == ImagePreset.QUALITY_FINAL) { + if (quality == FilterEnvironment.QUALITY_FINAL) { for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) { paint(strokeData, canvas, originalRotateToScreen, quality); } @@ -248,17 +249,17 @@ public class ImageFilterDraw extends ImageFilter { int n = v.size(); for (int i = mCachedStrokes; i < n; i++) { - paint(v.get(i), drawCache, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW); + paint(v.get(i), drawCache, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); } mCachedStrokes = n; } public void draw(Canvas canvas, Matrix originalRotateToScreen) { for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) { - paint(strokeData, canvas, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW); + paint(strokeData, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); } mDrawingsTypes[mCurrentStyle].paint( - null, canvas, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW); + null, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); } @Override diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java index 46a9a294c..82de2b73a 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java @@ -21,7 +21,7 @@ import android.graphics.Bitmap; import com.android.gallery3d.R; public class ImageFilterEdge extends SimpleImageFilter { - + private static final String SERIALIZATION_NAME = "EDGE"; public ImageFilterEdge() { mName = "Edge"; } @@ -29,6 +29,7 @@ public class ImageFilterEdge extends SimpleImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterRepresentation representation = super.getDefaultRepresentation(); representation.setName("Edge"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterEdge.class); representation.setTextId(R.string.edge); representation.setButtonId(R.id.edgeButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java index b0b0b2dd8..6fdcd249b 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java @@ -21,7 +21,7 @@ import com.android.gallery3d.R; import android.graphics.Bitmap; public class ImageFilterExposure extends SimpleImageFilter { - + private static final String SERIALIZATION_NAME = "EXPOSURE"; public ImageFilterExposure() { mName = "Exposure"; } @@ -30,6 +30,7 @@ public class ImageFilterExposure extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Exposure"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterExposure.class); representation.setTextId(R.string.exposure); representation.setButtonId(R.id.exposureButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java index 68e8a7c9d..51c66127b 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java @@ -37,6 +37,11 @@ public class ImageFilterFx extends ImageFilter { mFxBitmap = null; } + @Override + public FilterRepresentation getDefaultRepresentation() { + return null; + } + public void useRepresentation(FilterRepresentation representation) { FilterFxRepresentation parameters = (FilterFxRepresentation) representation; mParameters = parameters; @@ -87,4 +92,5 @@ public class ImageFilterFx extends ImageFilter { public void setResources(Resources resources) { mResources = resources; } + } diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java index 0022a9eae..0725dd1c4 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java @@ -21,6 +21,7 @@ import android.graphics.Bitmap; import com.android.gallery3d.R; public class ImageFilterHighlights extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "HIGHLIGHTS"; private static final String LOGTAG = "ImageFilterVignette"; public ImageFilterHighlights() { @@ -33,7 +34,8 @@ public class ImageFilterHighlights extends SimpleImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); - representation.setName("Shadows"); + representation.setName("Highlights"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterHighlights.class); representation.setTextId(R.string.highlight_recovery); representation.setButtonId(R.id.highlightRecoveryButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java index b1f9f7365..7e6f68548 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java @@ -22,6 +22,7 @@ import com.android.gallery3d.filtershow.editors.BasicEditor; import android.graphics.Bitmap; public class ImageFilterHue extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "HUE"; private ColorSpaceMatrix cmatrix = null; public ImageFilterHue() { @@ -33,6 +34,7 @@ public class ImageFilterHue extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Hue"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterHue.class); representation.setMinimum(-180); representation.setMaximum(180); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java index 29e6d162f..93813813f 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java @@ -22,6 +22,7 @@ import android.text.format.Time; import com.android.gallery3d.R; public class ImageFilterKMeans extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "KMEANS"; private int mSeed = 0; public ImageFilterKMeans() { @@ -36,6 +37,7 @@ public class ImageFilterKMeans extends SimpleImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("KMeans"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterKMeans.class); representation.setMaximum(20); representation.setMinimum(2); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java index c256686fb..0747190fa 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java @@ -6,13 +6,14 @@ import com.android.gallery3d.R; import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; public class ImageFilterNegative extends ImageFilter { - + private static final String SERIALIZATION_NAME = "NEGATIVE"; public ImageFilterNegative() { mName = "Negative"; } public FilterRepresentation getDefaultRepresentation() { FilterRepresentation representation = new FilterDirectRepresentation("Negative"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterNegative.class); representation.setTextId(R.string.negative); representation.setButtonId(R.id.negativeButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java index cfbb560c7..806f30435 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java @@ -22,13 +22,14 @@ import android.support.v8.renderscript.*; import android.util.Log; import android.content.res.Resources; import com.android.gallery3d.R; -import com.android.gallery3d.filtershow.cache.CachingPipeline; +import com.android.gallery3d.filtershow.presets.PipelineInterface; public abstract class ImageFilterRS extends ImageFilter { private static final String LOGTAG = "ImageFilterRS"; private boolean DEBUG = false; private int mLastInputWidth = 0; private int mLastInputHeight = 0; + private long mLastTimeCalled; public static boolean PERF_LOGGING = false; @@ -51,26 +52,36 @@ public abstract class ImageFilterRS extends ImageFilter { } protected RenderScript getRenderScriptContext() { - return CachingPipeline.getRenderScriptContext(); + PipelineInterface pipeline = getEnvironment().getPipeline(); + return pipeline.getRSContext(); } protected Allocation getInPixelsAllocation() { - CachingPipeline pipeline = getEnvironment().getCachingPipeline(); + PipelineInterface pipeline = getEnvironment().getPipeline(); return pipeline.getInPixelsAllocation(); } protected Allocation getOutPixelsAllocation() { - CachingPipeline pipeline = getEnvironment().getCachingPipeline(); + PipelineInterface pipeline = getEnvironment().getPipeline(); return pipeline.getOutPixelsAllocation(); } @Override public void apply(Allocation in, Allocation out) { long startOverAll = System.nanoTime(); + if (PERF_LOGGING) { + long delay = (startOverAll - mLastTimeCalled) / 1000; + String msg = String.format("%s; image size %dx%d; ", getName(), + in.getType().getX(), in.getType().getY()); + msg += String.format("called after %.2f ms (%.2f FPS); ", + delay / 1000.f, 1000000.f / delay); + Log.i(LOGTAG, msg); + } + mLastTimeCalled = startOverAll; long startFilter = 0; long endFilter = 0; if (!mResourcesLoaded) { - CachingPipeline pipeline = getEnvironment().getCachingPipeline(); + PipelineInterface pipeline = getEnvironment().getPipeline(); createFilter(pipeline.getResources(), getEnvironment().getScaleFactor(), getEnvironment().getQuality(), in); mResourcesLoaded = true; @@ -102,7 +113,7 @@ public abstract class ImageFilterRS extends ImageFilter { return bitmap; } try { - CachingPipeline pipeline = getEnvironment().getCachingPipeline(); + PipelineInterface pipeline = getEnvironment().getPipeline(); if (DEBUG) { Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName()); } @@ -137,18 +148,16 @@ public abstract class ImageFilterRS extends ImageFilter { displayLowMemoryToast(); Log.e(LOGTAG, "not enough memory for filter " + getName(), e); } - return bitmap; } - protected static Allocation convertBitmap(Bitmap bitmap) { - return Allocation.createFromBitmap(CachingPipeline.getRenderScriptContext(), bitmap, + protected static Allocation convertBitmap(RenderScript RS, Bitmap bitmap) { + return Allocation.createFromBitmap(RS, bitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE); } - private static Allocation convertRGBAtoA(Bitmap bitmap) { - RenderScript RS = CachingPipeline.getRenderScriptContext(); + private static Allocation convertRGBAtoA(RenderScript RS, Bitmap bitmap) { if (RS != mRScache || mGreyConvert == null) { mGreyConvert = new ScriptC_grey(RS, RS.getApplicationContext().getResources(), R.raw.grey); @@ -157,7 +166,7 @@ public abstract class ImageFilterRS extends ImageFilter { Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS)); - Allocation bitmapTemp = convertBitmap(bitmap); + Allocation bitmapTemp = convertBitmap(RS, bitmap); if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) { return bitmapTemp; } @@ -173,20 +182,20 @@ public abstract class ImageFilterRS extends ImageFilter { } public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) { - Resources res = CachingPipeline.getResources(); + Resources res = getEnvironment().getPipeline().getResources(); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ALPHA_8; options.inSampleSize = inSampleSize; Bitmap bitmap = BitmapFactory.decodeResource( res, resource, options); - Allocation ret = convertRGBAtoA(bitmap); + Allocation ret = convertRGBAtoA(getRenderScriptContext(), bitmap); bitmap.recycle(); return ret; } public Allocation loadScaledResourceAlpha(int resource, int w, int h, int inSampleSize) { - Resources res = CachingPipeline.getResources(); + Resources res = getEnvironment().getPipeline().getResources(); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ALPHA_8; options.inSampleSize = inSampleSize; @@ -194,7 +203,7 @@ public abstract class ImageFilterRS extends ImageFilter { res, resource, options); Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); - Allocation ret = convertRGBAtoA(resizeBitmap); + Allocation ret = convertRGBAtoA(getRenderScriptContext(), resizeBitmap); resizeBitmap.recycle(); bitmap.recycle(); return ret; @@ -205,13 +214,13 @@ public abstract class ImageFilterRS extends ImageFilter { } public Allocation loadResource(int resource) { - Resources res = CachingPipeline.getResources(); + Resources res = getEnvironment().getPipeline().getResources(); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap = BitmapFactory.decodeResource( res, resource, options); - Allocation ret = convertBitmap(bitmap); + Allocation ret = convertBitmap(getRenderScriptContext(), bitmap); bitmap.recycle(); return ret; } @@ -232,7 +241,7 @@ public abstract class ImageFilterRS extends ImageFilter { /** * RS Script objects (and all other RS objects) should be cleared here */ - abstract protected void resetScripts(); + public abstract void resetScripts(); /** * Scripts values should be bound here diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java index 0febe4957..adc74c5ec 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java @@ -21,7 +21,7 @@ import com.android.gallery3d.R; import android.graphics.Bitmap; public class ImageFilterSaturated extends SimpleImageFilter { - + private static final String SERIALIZATION_NAME = "SATURATED"; public ImageFilterSaturated() { mName = "Saturated"; } @@ -31,6 +31,7 @@ public class ImageFilterSaturated extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Saturated"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterSaturated.class); representation.setTextId(R.string.saturation); representation.setButtonId(R.id.saturationButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java index fd67ee8fc..845290b80 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java @@ -21,7 +21,7 @@ import com.android.gallery3d.R; import android.graphics.Bitmap; public class ImageFilterShadows extends SimpleImageFilter { - + private static final String SERIALIZATION_NAME = "SHADOWS"; public ImageFilterShadows() { mName = "Shadows"; @@ -31,6 +31,7 @@ public class ImageFilterShadows extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Shadows"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterShadows.class); representation.setTextId(R.string.shadow_recovery); representation.setButtonId(R.id.shadowRecoveryButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java index 76ae475ac..1dc2c0516 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java @@ -19,7 +19,7 @@ package com.android.gallery3d.filtershow.filters; import com.android.gallery3d.R; public class ImageFilterSharpen extends ImageFilterRS { - + private static final String SERIALIZATION_NAME = "SHARPEN"; private static final String LOGTAG = "ImageFilterSharpen"; private ScriptC_convolve3x3 mScript; @@ -31,6 +31,7 @@ public class ImageFilterSharpen extends ImageFilterRS { public FilterRepresentation getDefaultRepresentation() { FilterRepresentation representation = new FilterBasicRepresentation("Sharpen", 0, 0, 100); + representation.setSerializationName(SERIALIZATION_NAME); representation.setShowParameterValue(true); representation.setFilterClass(ImageFilterSharpen.class); representation.setTextId(R.string.sharpness); @@ -52,7 +53,7 @@ public class ImageFilterSharpen extends ImageFilterRS { } @Override - protected void resetScripts() { + public void resetScripts() { if (mScript != null) { mScript.destroy(); mScript = null; diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java index 37d5739a0..f265c4dcc 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java @@ -76,7 +76,7 @@ public class ImageFilterTinyPlanet extends SimpleImageFilter { int w = bitmapIn.getWidth(); int h = bitmapIn.getHeight(); int outputSize = (int) (w / 2f); - ImagePreset preset = getImagePreset(); + ImagePreset preset = getEnvironment().getImagePreset(); Bitmap mBitmapOut = null; if (preset != null) { XMPMeta xmp = preset.getImageLoader().getXmpObject(); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java index ea315d326..900fd906c 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java @@ -21,7 +21,7 @@ import com.android.gallery3d.R; import android.graphics.Bitmap; public class ImageFilterVibrance extends SimpleImageFilter { - + private static final String SERIALIZATION_NAME = "VIBRANCE"; public ImageFilterVibrance() { mName = "Vibrance"; } @@ -30,6 +30,7 @@ public class ImageFilterVibrance extends SimpleImageFilter { FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); representation.setName("Vibrance"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterVibrance.class); representation.setTextId(R.string.vibrance); representation.setButtonId(R.id.vibranceButton); diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java index e06f54493..cfe135033 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java @@ -22,7 +22,7 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import com.android.gallery3d.R; -import com.android.gallery3d.filtershow.presets.ImagePreset; +import com.android.gallery3d.filtershow.presets.FilterEnvironment; public class ImageFilterVignette extends SimpleImageFilter { private static final String LOGTAG = "ImageFilterVignette"; @@ -57,9 +57,9 @@ public class ImageFilterVignette extends SimpleImageFilter { @Override public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { - if (SIMPLE_ICONS && ImagePreset.QUALITY_ICON == quality) { + if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) { if (mOverlayBitmap == null) { - Resources res = getEnvironment().getCachingPipeline().getResources(); + Resources res = getEnvironment().getPipeline().getResources(); mOverlayBitmap = IconUtilities.getFXBitmap(res, R.drawable.filtershow_icon_vignette); } diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java index c4c293a4b..84a14c902 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java @@ -22,6 +22,7 @@ import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; import android.graphics.Bitmap; public class ImageFilterWBalance extends ImageFilter { + private static final String SERIALIZATION_NAME = "WBALANCE"; private static final String TAG = "ImageFilterWBalance"; public ImageFilterWBalance() { @@ -30,6 +31,7 @@ public class ImageFilterWBalance extends ImageFilter { public FilterRepresentation getDefaultRepresentation() { FilterRepresentation representation = new FilterDirectRepresentation("WBalance"); + representation.setSerializationName(SERIALIZATION_NAME); representation.setFilterClass(ImageFilterWBalance.class); representation.setPriority(FilterRepresentation.TYPE_WBALANCE); representation.setTextId(R.string.wbalance); diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java index 898fdf021..b0b4c4fe5 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java +++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java @@ -20,6 +20,7 @@ import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; +import android.util.Log; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.crop.CropExtras; @@ -30,7 +31,14 @@ import com.android.gallery3d.filtershow.editors.EditorStraighten; import com.android.gallery3d.filtershow.filters.FilterRepresentation; import com.android.gallery3d.filtershow.filters.ImageFilterGeometry; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; + public class GeometryMetadata extends FilterRepresentation { + private static final String SERIALIZATION_NAME = "GEOM"; private static final String LOGTAG = "GeometryMetadata"; private float mScaleFactor = 1.0f; private float mRotation = 0; @@ -40,7 +48,26 @@ public class GeometryMetadata extends FilterRepresentation { private FLIP mFlip = FLIP.NONE; public enum FLIP { - NONE, VERTICAL, HORIZONTAL, BOTH + NONE("N"), VERTICAL("V"), HORIZONTAL("H"), BOTH("B"); + String mValue; + + FLIP(String name) { + mValue = name; + } + + public static FLIP parse(String name){ + switch (name.charAt(0)) { + case 'N': + return NONE; + case 'V': + return VERTICAL; + case 'H': + return HORIZONTAL; + case 'B': + return BOTH; + }; + return NONE; + } } // Output format data from intent extras @@ -64,6 +91,7 @@ public class GeometryMetadata extends FilterRepresentation { public GeometryMetadata() { super("GeometryMetadata"); + setSerializationName(SERIALIZATION_NAME); setFilterClass(ImageFilterGeometry.class); setEditorId(EditorCrop.ID); setTextId(0); @@ -468,4 +496,87 @@ public class GeometryMetadata extends FilterRepresentation { representation.useParametersFrom(this); return representation; } -} + + private static final String[] sParams = { + "Name", "ScaleFactor", "Rotation", "StraightenRotation", "CropBoundsLeft", + "CropBoundsTop", "CropBoundsRight", "CropBoundsBottom", "PhotoBoundsLeft", + "PhotoBoundsTop", "PhotoBoundsRight", "PhotoBoundsBottom", "Flip" + }; + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + { "Name", getName() }, + { "ScaleFactor", Float.toString(mScaleFactor) }, + { "Rotation", Float.toString(mRotation) }, + { "StraightenRotation", Float.toString(mStraightenRotation) }, + { "CropBoundsLeft", Float.toString(mCropBounds.left) }, + { "CropBoundsTop", Float.toString(mCropBounds.top) }, + { "CropBoundsRight", Float.toString(mCropBounds.right) }, + { "CropBoundsBottom", Float.toString(mCropBounds.bottom) }, + { "PhotoBoundsLeft", Float.toString(mPhotoBounds.left) }, + { "PhotoBoundsTop", Float.toString(mPhotoBounds.top) }, + { "PhotoBoundsRight", Float.toString(mPhotoBounds.right) }, + { "PhotoBoundsBottom", Float.toString(mPhotoBounds.bottom) }, + { "Flip", mFlip.mValue } }; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + HashMap<String, Integer> map = new HashMap<String, Integer>(); + for (int i = 0; i < sParams.length; i++) { + map.put(sParams[i], i); + } + for (int i = 0; i < rep.length; i++) { + String key = rep[i][0]; + String value = rep[i][1]; + + switch (map.get(key)) { + case -1: // Unknown + break; + case 0: + if (!getName().equals(value)) { + throw new IllegalArgumentException("Not a "+getName()); + } + break; + case 1: // "ScaleFactor", Float + mScaleFactor = Float.parseFloat(value); + break; + case 2: // "Rotation", Float + mRotation = Float.parseFloat(value); + break; + case 3: // "StraightenRotation", Float + mStraightenRotation = Float.parseFloat(value); + break; + case 4: // "mCropBoundsLeft", Float + mCropBounds.left = Float.parseFloat(value); + break; + case 5: // "mCropBoundsTop", Float + mCropBounds.top = Float.parseFloat(value); + break; + case 6: // "mCropBoundsRight", Float + mCropBounds.right = Float.parseFloat(value); + break; + case 7: // "mCropBoundsBottom", Float + mCropBounds.bottom = Float.parseFloat(value); + break; + case 8: // "mPhotoBoundsLeft", Float + mPhotoBounds.left = Float.parseFloat(value); + break; + case 9: // "mPhotoBoundsTop", Float + mPhotoBounds.top = Float.parseFloat(value); + break; + case 10: // "mPhotoBoundsRight", Float + mPhotoBounds.right = Float.parseFloat(value); + break; + case 11: // "mPhotoBoundsBottom", Float + mPhotoBounds.bottom = Float.parseFloat(value); + break; + case 12: // "Flip", enum + mFlip = FLIP.parse(value); + break; + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java index 5e5d70b7c..7e086b0ca 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java +++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java @@ -20,6 +20,7 @@ import android.graphics.*; import android.os.Handler; import android.os.Message; +import android.util.Log; import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.HistoryAdapter; import com.android.gallery3d.filtershow.cache.*; @@ -139,6 +140,7 @@ public class MasterImage implements RenderingRequestCaller { } public synchronized void setPreset(ImagePreset preset, boolean addToHistory) { + preset.showFilters(); mPreset = preset; mPreset.setImageLoader(mLoader); setGeometry(); diff --git a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java index c454c1ab6..47f8dfccb 100644 --- a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java +++ b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java @@ -18,11 +18,9 @@ package com.android.gallery3d.filtershow.presets; import android.graphics.Bitmap; import android.support.v8.renderscript.Allocation; -import android.util.Log; -import com.android.gallery3d.filtershow.cache.CachingPipeline; import com.android.gallery3d.filtershow.filters.FilterRepresentation; -import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.FiltersManagerInterface; import com.android.gallery3d.filtershow.filters.ImageFilter; import java.lang.ref.WeakReference; @@ -33,10 +31,14 @@ public class FilterEnvironment { private ImagePreset mImagePreset; private float mScaleFactor; private int mQuality; - private FiltersManager mFiltersManager; - private CachingPipeline mCachingPipeline; + private FiltersManagerInterface mFiltersManager; + private PipelineInterface mPipeline; private volatile boolean mStop = false; + public static final int QUALITY_ICON = 0; + public static final int QUALITY_PREVIEW = 1; + public static final int QUALITY_FINAL = 2; + public synchronized boolean needsStop() { return mStop; } @@ -98,11 +100,11 @@ public class FilterEnvironment { return mQuality; } - public void setFiltersManager(FiltersManager filtersManager) { + public void setFiltersManager(FiltersManagerInterface filtersManager) { mFiltersManager = filtersManager; } - public FiltersManager getFiltersManager() { + public FiltersManagerInterface getFiltersManager() { return mFiltersManager; } @@ -126,12 +128,12 @@ public class FilterEnvironment { return ret; } - public CachingPipeline getCachingPipeline() { - return mCachingPipeline; + public PipelineInterface getPipeline() { + return mPipeline; } - public void setCachingPipeline(CachingPipeline cachingPipeline) { - mCachingPipeline = cachingPipeline; + public void setPipeline(PipelineInterface cachingPipeline) { + mPipeline = cachingPipeline; } } diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java index 3530935e1..b594d9a02 100644 --- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java +++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java @@ -18,12 +18,19 @@ package com.android.gallery3d.filtershow.presets; import android.graphics.Bitmap; import android.graphics.Rect; +import android.net.Uri; import android.support.v8.renderscript.Allocation; +import android.util.JsonReader; +import android.util.JsonWriter; import android.util.Log; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.options.PropertyOptions; import com.android.gallery3d.filtershow.cache.CachingPipeline; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.filters.BaseFiltersManager; +import com.android.gallery3d.filtershow.filters.FiltersManager; import com.android.gallery3d.filtershow.filters.FilterRepresentation; import com.android.gallery3d.filtershow.filters.ImageFilter; import com.android.gallery3d.filtershow.imageshow.GeometryMetadata; @@ -32,6 +39,10 @@ import com.android.gallery3d.filtershow.state.State; import com.android.gallery3d.filtershow.state.StateAdapter; import com.android.gallery3d.util.UsageStatistics; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; import java.util.Vector; public class ImagePreset { @@ -39,10 +50,8 @@ public class ImagePreset { private static final String LOGTAG = "ImagePreset"; private FilterRepresentation mBorder = null; - public static final int QUALITY_ICON = 0; - public static final int QUALITY_PREVIEW = 1; - public static final int QUALITY_FINAL = 2; public static final int STYLE_ICON = 3; + public static final String PRESET_NAME = "PresetName"; private ImageLoader mImageLoader = null; @@ -210,11 +219,11 @@ public class ImagePreset { } for (FilterRepresentation representation : mFilters) { if (representation.getPriority() == FilterRepresentation.TYPE_VIGNETTE - && !representation.isNil()) { + && !representation.isNil()) { return false; } if (representation.getPriority() == FilterRepresentation.TYPE_TINYPLANET - && !representation.isNil()) { + && !representation.isNil()) { return false; } } @@ -460,7 +469,7 @@ public class ImagePreset { if (mBorder != null && mDoApplyGeometry) { mBorder.synchronizeRepresentation(); bitmap = environment.applyRepresentation(mBorder, bitmap); - if (environment.getQuality() == QUALITY_FINAL) { + if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) { UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, "SaveBorder", mBorder.getName(), 1); } @@ -468,6 +477,10 @@ public class ImagePreset { return bitmap; } + public int nbFilters() { + return mFilters.size(); + } + public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) { if (mDoApplyFilters) { if (from < 0) { @@ -483,7 +496,7 @@ public class ImagePreset { representation.synchronizeRepresentation(); } bitmap = environment.applyRepresentation(representation, bitmap); - if (environment.getQuality() == QUALITY_FINAL) { + if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) { UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, "SaveFilter", representation.getName(), 1); } @@ -496,17 +509,23 @@ public class ImagePreset { return bitmap; } - public void applyBorder(Allocation in, Allocation out, FilterEnvironment environment) { + public void applyBorder(Allocation in, Allocation out, + boolean copyOut, FilterEnvironment environment) { if (mBorder != null && mDoApplyGeometry) { mBorder.synchronizeRepresentation(); // TODO: should keep the bitmap around - Allocation bitmapIn = Allocation.createTyped(CachingPipeline.getRenderScriptContext(), in.getType()); - bitmapIn.copyFrom(out); + Allocation bitmapIn = in; + if (copyOut) { + bitmapIn = Allocation.createTyped( + CachingPipeline.getRenderScriptContext(), in.getType()); + bitmapIn.copyFrom(out); + } environment.applyRepresentation(mBorder, bitmapIn, out); } } - public void applyFilters(int from, int to, Allocation in, Allocation out, FilterEnvironment environment) { + public void applyFilters(int from, int to, Allocation in, Allocation out, + FilterEnvironment environment) { if (mDoApplyFilters) { if (from < 0) { from = 0; @@ -601,4 +620,109 @@ public class ImagePreset { return usedFilters; } + public String getJsonString(String name) { + StringWriter swriter = new StringWriter(); + try { + JsonWriter writer = new JsonWriter(swriter); + writeJson(writer, name); + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + return swriter.toString(); + } + + public void writeJson(JsonWriter writer, String name) { + int numFilters = mFilters.size(); + try { + writer.beginObject(); + writer.name(PRESET_NAME).value(name); + writer.name(mGeoData.getSerializationName()); + writer.beginObject(); + { + String[][] rep = mGeoData.serializeRepresentation(); + for (int i = 0; i < rep.length; i++) { + writer.name(rep[i][0]); + writer.value(rep[i][1]); + } + } + writer.endObject(); + + for (int i = 0; i < numFilters; i++) { + FilterRepresentation filter = mFilters.get(i); + String sname = filter.getSerializationName(); + writer.name(sname); + writer.beginObject(); + { + String[][] rep = filter.serializeRepresentation(); + for (int k = 0; k < rep.length; k++) { + writer.name(rep[k][0]); + writer.value(rep[k][1]); + } + } + writer.endObject(); + } + writer.endObject(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean readJsonFromString(String filterString) { + StringReader sreader = new StringReader(filterString); + try { + JsonReader reader = new JsonReader(sreader); + boolean ok = readJson(reader); + if (!ok) { + return false; + } + reader.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + + public boolean readJson(JsonReader sreader) throws IOException { + sreader.beginObject(); + sreader.nextName(); + mName = sreader.nextString(); + + while (sreader.hasNext()) { + String name = sreader.nextName(); + + if (mGeoData.getSerializationName().equals(name)) { + mGeoData.deSerializeRepresentation(read(sreader)); + } else { + FilterRepresentation filter = creatFilterFromName(name); + if (filter == null) + return false; + filter.deSerializeRepresentation(read(sreader)); + addFilter(filter); + } + } + sreader.endObject(); + return true; + } + + FilterRepresentation creatFilterFromName(String name) { + FiltersManager filtersManager = FiltersManager.getManager(); + return filtersManager.createFilterFromName(name); + } + + String[][] read(JsonReader reader) throws IOException { + ArrayList <String[]> al = new ArrayList<String[]>(); + + reader.beginObject(); + + while (reader.hasNext()) { + String[]kv = { reader.nextName(),reader.nextString()}; + al.add(kv); + + } + reader.endObject(); + return al.toArray(new String[al.size()][]); + } } diff --git a/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java b/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java new file mode 100644 index 000000000..05f0a1aa8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java @@ -0,0 +1,31 @@ +/* + * 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.gallery3d.filtershow.presets; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.RenderScript; + +public interface PipelineInterface { + public String getName(); + public Resources getResources(); + public Allocation getInPixelsAllocation(); + public Allocation getOutPixelsAllocation(); + public boolean prepareRenderscriptAllocations(Bitmap bitmap); + public RenderScript getRSContext(); +} diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java index c5851c476..b5de6929a 100644 --- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java +++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java @@ -206,6 +206,8 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { uri = insertContent(context, sourceUri, this.destinationFile, saveFileName, time); } + XmpPresets.writeFilterXMP(context, sourceUri, this.destinationFile, preset); + noBitmap = false; } catch (java.lang.OutOfMemoryError e) { // Try 5 times before failing for good. @@ -219,6 +221,7 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> { return uri; } + @Override protected void onPostExecute(Uri result) { if (callback != null) { diff --git a/src/com/android/gallery3d/filtershow/tools/XmpPresets.java b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java new file mode 100644 index 000000000..be75f0253 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java @@ -0,0 +1,133 @@ +/* + * 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.gallery3d.filtershow.tools; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.presets.ImagePreset; +import com.android.gallery3d.util.XmpUtilHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class XmpPresets { + public static final String + XMP_GOOGLE_FILTER_NAMESPACE = "http://ns.google.com/photos/1.0/filter/"; + public static final String XMP_GOOGLE_FILTER_PREFIX = "AFltr"; + public static final String XMP_SRC_FILE_URI = "SourceFileUri"; + public static final String XMP_FILTERSTACK = "filterstack"; + private static final String LOGTAG = "XmpPresets"; + + public static class XMresults { + public String presetString; + public ImagePreset preset; + public Uri originalimage; + } + + static { + try { + XMPMetaFactory.getSchemaRegistry().registerNamespace( + XMP_GOOGLE_FILTER_NAMESPACE, XMP_GOOGLE_FILTER_PREFIX); + } catch (XMPException e) { + Log.e(LOGTAG, "Register XMP name space failed", e); + } + } + + public static void writeFilterXMP( + Context context, Uri srcUri, File dstFile, ImagePreset preset) { + InputStream is = null; + XMPMeta xmpMeta = null; + try { + is = context.getContentResolver().openInputStream(srcUri); + xmpMeta = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + + } finally { + Utils.closeSilently(is); + } + + if (xmpMeta == null) { + xmpMeta = XMPMetaFactory.create(); + } + try { + xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_SRC_FILE_URI, srcUri.toString()); + xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_FILTERSTACK, preset.getJsonString(context.getString(R.string.saved))); + } catch (XMPException e) { + Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath()); + return; + } + + if (!XmpUtilHelper.writeXMPMeta(dstFile.getAbsolutePath(), xmpMeta)) { + Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath()); + } + } + + public static XMresults extractXMPData( + Context context, MasterImage mMasterImage, Uri uriToEdit) { + XMresults ret = new XMresults(); + + InputStream is = null; + XMPMeta xmpMeta = null; + try { + is = context.getContentResolver().openInputStream(uriToEdit); + xmpMeta = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + } finally { + Utils.closeSilently(is); + } + + if (xmpMeta == null) { + return null; + } + + try { + String strSrcUri = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_SRC_FILE_URI); + + if (strSrcUri != null) { + String filterString = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_FILTERSTACK); + + Uri srcUri = Uri.parse(strSrcUri); + ret.originalimage = srcUri; + + ret.preset = new ImagePreset(mMasterImage.getPreset()); + ret.presetString = filterString; + boolean ok = ret.preset.readJsonFromString(filterString); + if (!ok) { + return null; + } + return ret; + } + } catch (XMPException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java index e5e77beb4..f5d3dc397 100644 --- a/src/com/android/gallery3d/ui/MenuExecutor.java +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -190,7 +190,7 @@ public class MenuExecutor { setMenuItemVisible(menu, R.id.action_setas, supportSetAs); setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap); setMenuItemVisible(menu, R.id.action_edit, supportEdit); - // setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit); + setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit); setMenuItemVisible(menu, R.id.action_details, supportInfo); } diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java index c281dd3e7..e2c5f51b9 100644 --- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java +++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java @@ -19,6 +19,7 @@ package com.android.gallery3d.util; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; +import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore.Video; @@ -95,7 +96,7 @@ public class SaveVideoFileUtils { ContentResolver contentResolver, Uri uri ) { long nowInMs = System.currentTimeMillis(); long nowInSec = nowInMs / 1000; - final ContentValues values = new ContentValues(12); + final ContentValues values = new ContentValues(13); values.put(Video.Media.TITLE, mDstFileInfo.mFileName); values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName()); values.put(Video.Media.MIME_TYPE, "video/mp4"); @@ -104,6 +105,8 @@ public class SaveVideoFileUtils { values.put(Video.Media.DATE_ADDED, nowInSec); values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath()); values.put(Video.Media.SIZE, mDstFileInfo.mFile.length()); + int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath()); + values.put(Video.Media.DURATION, durationMs); // Copy the data taken and location info from src. String[] projection = new String[] { VideoColumns.DATE_TAKEN, @@ -138,4 +141,18 @@ public class SaveVideoFileUtils { return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values); } + public static int retriveVideoDurationMs(String path) { + int durationMs = 0; + // Calculate the duration of the destination file. + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(path); + String duration = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION); + if (duration != null) { + durationMs = Integer.parseInt(duration); + } + retriever.release(); + return durationMs; + } + } |