am 8bb4e1a0: Fixes reading exif tags in editor.
* commit '8bb4e1a036e67a3bf4f9cdb8f55d6f558189e5b9':
Fixes reading exif tags in editor.
diff --git a/src/com/android/camera/NewCameraActivity.java b/src/com/android/camera/NewCameraActivity.java
new file mode 100644
index 0000000..3aee150
--- /dev/null
+++ b/src/com/android/camera/NewCameraActivity.java
@@ -0,0 +1,374 @@
+/*
+ * 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.ContentResolver;
+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.net.Uri;
+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 static final String TAG = "CAM_Activity";
+ 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;
+ }
+
+ public void notifyNewMedia(Uri uri) {
+ ContentResolver cr = getContentResolver();
+ String mimeType = cr.getType(uri);
+ if (mimeType.startsWith("video/")) {
+ sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO, uri));
+ mDataAdapter.addNewVideo(cr, uri);
+ } else if (mimeType.startsWith("image/")) {
+ Util.broadcastNewPicture(this, uri);
+ mDataAdapter.addNewPhoto(cr, uri);
+ } else {
+ android.util.Log.w(TAG, "Unknown new media with MIME type:"
+ + mimeType + ", uri:" + uri);
+ }
+ }
+
+ 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);
+ mFilmStripView.setViewGap(
+ getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap));
+ // 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);
+ mFilmStripView.setListener(new FilmStripView.Listener() {
+ @Override
+ public void onDataPromoted(int dataID) {
+ mDataAdapter.removeData(dataID);
+ }
+
+ @Override
+ public void onDataDemoted(int dataID) {
+ mDataAdapter.removeData(dataID);
+ }
+
+ });
+ 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 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();
+ }
+
+ 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 0000000..35452bb
--- /dev/null
+++ b/src/com/android/camera/NewCameraModule.java
@@ -0,0 +1,74 @@
+/*
+ * 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 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 0000000..c63aff4
--- /dev/null
+++ b/src/com/android/camera/NewPhotoMenu.java
@@ -0,0 +1,200 @@
+/*
+ * 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.res.Resources;
+import android.hardware.Camera.Parameters;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CountdownTimerPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.gallery3d.R;
+
+import java.util.Locale;
+
+public class NewPhotoMenu extends PieController
+ implements CountdownTimerPopup.Listener,
+ ListPrefSettingPopup.Listener {
+ private static String TAG = "CAM_photomenu";
+
+ private final String mSettingOff;
+
+ private NewPhotoUI mUI;
+ private AbstractSettingPopup mPopup;
+ 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;
+ PieItem item = null;
+ final Resources res = mActivity.getResources();
+ Locale locale = res.getConfiguration().locale;
+ // the order is from left to right in the menu
+
+ // hdr
+ if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_CAMERA_HDR, true);
+ mRenderer.addItem(item);
+ }
+ // exposure compensation
+ if (group.findPreference(CameraSettings.KEY_EXPOSURE) != null) {
+ item = makeItem(CameraSettings.KEY_EXPOSURE);
+ item.setLabel(res.getString(R.string.pref_exposure_label));
+ mRenderer.addItem(item);
+ }
+ // more settings
+ PieItem more = makeItem(R.drawable.ic_settings_holo_light);
+ more.setLabel(res.getString(R.string.camera_menu_more_label));
+ mRenderer.addItem(more);
+ // flash
+ if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) {
+ item = makeItem(CameraSettings.KEY_FLASH_MODE);
+ item.setLabel(res.getString(R.string.pref_camera_flashmode_label));
+ mRenderer.addItem(item);
+ }
+ // camera switcher
+ if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_CAMERA_ID, false);
+ final PieItem fitem = item;
+ 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;
+ pref.setValueIndex(index);
+ mListener.onCameraPickerClicked(index);
+ }
+ updateItem(fitem, CameraSettings.KEY_CAMERA_ID);
+ }
+ });
+ mRenderer.addItem(item);
+ }
+ // location
+ if (group.findPreference(CameraSettings.KEY_RECORD_LOCATION) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_RECORD_LOCATION, true);
+ more.addItem(item);
+ if (mActivity.isSecureCamera()) {
+ // Prevent location preference from getting changed in secure camera mode
+ item.setEnabled(false);
+ }
+ }
+ // countdown timer
+ final ListPreference ctpref = group.findPreference(CameraSettings.KEY_TIMER);
+ final ListPreference beeppref = group.findPreference(CameraSettings.KEY_TIMER_SOUND_EFFECTS);
+ item = makeItem(R.drawable.ic_timer);
+ item.setLabel(res.getString(R.string.pref_camera_timer_title).toUpperCase(locale));
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ CountdownTimerPopup timerPopup = (CountdownTimerPopup) mActivity.getLayoutInflater().inflate(
+ R.layout.countdown_setting_popup, null, false);
+ timerPopup.initialize(ctpref, beeppref);
+ timerPopup.setSettingChangedListener(NewPhotoMenu.this);
+ mUI.dismissPopup();
+ mPopup = timerPopup;
+ mUI.showPopup(mPopup);
+ }
+ });
+ more.addItem(item);
+ // image size
+ item = makeItem(R.drawable.ic_imagesize);
+ final ListPreference sizePref = group.findPreference(CameraSettings.KEY_PICTURE_SIZE);
+ item.setLabel(res.getString(R.string.pref_camera_picturesize_title).toUpperCase(locale));
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ ListPrefSettingPopup popup = (ListPrefSettingPopup) mActivity.getLayoutInflater().inflate(
+ R.layout.list_pref_setting_popup, null, false);
+ popup.initialize(sizePref);
+ popup.setSettingChangedListener(NewPhotoMenu.this);
+ mUI.dismissPopup();
+ mPopup = popup;
+ mUI.showPopup(mPopup);
+ }
+ });
+ more.addItem(item);
+ // white balance
+ if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+ item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+ item.setLabel(res.getString(R.string.pref_camera_whitebalance_label));
+ more.addItem(item);
+ }
+ // scene mode
+ if (group.findPreference(CameraSettings.KEY_SCENE_MODE) != null) {
+ IconListPreference pref = (IconListPreference) group.findPreference(
+ CameraSettings.KEY_SCENE_MODE);
+ pref.setUseSingleIcon(true);
+ item = makeItem(CameraSettings.KEY_SCENE_MODE);
+ more.addItem(item);
+ }
+ }
+
+ @Override
+ // Hit when an item in a popup gets selected
+ public void onListPrefChanged(ListPreference pref) {
+ if (mPopup != null) {
+ mUI.dismissPopup();
+ }
+ onSettingChanged(pref);
+ }
+
+ public void popupDismissed() {
+ if (mPopup != null) {
+ mPopup = null;
+ }
+ }
+
+ // 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);
+ }
+}
diff --git a/src/com/android/camera/NewPhotoModule.java b/src/com/android/camera/NewPhotoModule.java
new file mode 100644
index 0000000..ecbbbf3
--- /dev/null
+++ b/src/com/android/camera/NewPhotoModule.java
@@ -0,0 +1,2031 @@
+/*
+ * 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.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+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;
+ private static final int CAPTURE_ANIMATION_DONE = 13;
+
+ // The subset of parameters we need to update in setCameraParameters().
+ private static final int UPDATE_PARAM_INITIALIZE = 1;
+ private static final int UPDATE_PARAM_ZOOM = 2;
+ private static final int UPDATE_PARAM_PREFERENCE = 4;
+ private static final int UPDATE_PARAM_ALL = -1;
+
+ // This is the timeout to keep the camera in onPause for the first time
+ // after screen on if the activity is started from secure lock screen.
+ private static final int KEEP_CAMERA_TIMEOUT = 1000; // ms
+
+ // copied from Camera hierarchy
+ private NewCameraActivity mActivity;
+ private CameraProxy mCameraDevice;
+ private int mCameraId;
+ private Parameters mParameters;
+ private boolean mPaused;
+
+ private NewPhotoUI mUI;
+
+ // The activity is going to switch to the specified camera id. This is
+ // needed because texture copy is done in GL thread. -1 means camera is not
+ // switching.
+ protected int mPendingSwitchCameraId = -1;
+ private boolean mOpenCameraFail;
+ private boolean mCameraDisabled;
+
+ // When setCameraParametersWhenIdle() is called, we accumulate the subsets
+ // needed to be updated in mUpdateSet.
+ private int mUpdateSet;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+ private int mZoomValue; // The current zoom value.
+
+ private 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 Runnable mFlashRunnable = new Runnable() {
+ @Override
+ public void run() {
+ animateFlash();
+ }
+ };
+
+ 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 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);
+ mActivity.notifyNewMedia(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;
+ }
+ case CAPTURE_ANIMATION_DONE: {
+ mUI.enablePreviewThumb(false);
+ 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();
+ }
+ }
+
+ private final class ShutterCallback
+ implements android.hardware.Camera.ShutterCallback {
+
+ private boolean mAnimateFlash;
+
+ public ShutterCallback(boolean animateFlash) {
+ mAnimateFlash = animateFlash;
+ }
+
+ @Override
+ public void onShutter() {
+ mShutterCallbackTime = System.currentTimeMillis();
+ mShutterLag = mShutterCallbackTime - mCaptureStartTime;
+ Log.v(TAG, "mShutterLag = " + mShutterLag + "ms");
+ if (mAnimateFlash) {
+ mActivity.runOnUiThread(mFlashRunnable);
+ }
+ }
+ }
+
+ 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
+ mHandler.removeMessages(CAPTURE_ANIMATION_DONE);
+ ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide();
+ mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+ CaptureAnimManager.getAnimationDuration());
+ } */
+ 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.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);
+ mUI.enablePreviewThumb(true);
+ mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+ CaptureAnimManager.getAnimationDuration());
+ } */
+ }
+
+ @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;
+ // 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(new ShutterCallback(!animateBefore),
+ mRawPictureCallback, mPostViewPictureCallback,
+ new JpegPictureCallback(loc), mCameraState,
+ mFocusManager.getFocusState());
+
+ mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime);
+
+ mFaceDetectionStarted = false;
+ setCameraState(SNAPSHOT_IN_PROGRESS);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+ UsageStatistics.ACTION_CAPTURE_DONE, "Photo");
+ 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(CropActivity.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) {
+ 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 (mSceneMode == Util.SCENE_MODE_HDR) {
+ mUI.hideSwitcher();
+ //TODO: mActivity.setSwipingEnabled(false);
+ }
+ // 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
+ if (mHandler.getLooper() == Looper.myLooper()) {
+ // On UI thread only, not when camera starts up
+ setupPreview();
+ } else {
+ 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, mPreferenceGroup, 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 0000000..d6e5057
--- /dev/null
+++ b/src/com/android/camera/NewPhotoUI.java
@@ -0,0 +1,783 @@
+/*
+ * 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,
+ FocusUI, TextureView.SurfaceTextureListener,
+ LocationManager.Listener,
+ FaceDetectionListener {
+
+ 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 OnScreenIndicators 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 View mPreviewThumb;
+
+ 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 = new OnScreenIndicators(mActivity,
+ mRootView.findViewById(R.id.on_screen_indicators));
+ }
+
+ 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);
+ mRenderOverlay.setGestures(mGestures);
+ }
+ mGestures.setRenderOverlay(mRenderOverlay);
+ mRenderOverlay.requestLayout();
+
+ initializeZoom(params);
+ updateOnScreenIndicators(params, prefGroup, 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);
+ mPreviewThumb = mActivity.findViewById(R.id.preview_thumb);
+ mPreviewThumb.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: go to filmstrip
+ // mActivity.gotoGallery();
+ }
+ });
+ 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,
+ PreferenceGroup group, ComboPreferences prefs) {
+ if (params == null) return;
+ mOnScreenIndicators.updateSceneOnScreenIndicator(params.getSceneMode());
+ mOnScreenIndicators.updateExposureOnScreenIndicator(params,
+ CameraSettings.readExposure(prefs));
+ mOnScreenIndicators.updateFlashOnScreenIndicator(params.getFlashMode());
+ int wbIndex = 2;
+ ListPreference pref = group.findPreference(CameraSettings.KEY_WHITE_BALANCE);
+ if (pref != null) {
+ wbIndex = pref.getCurrentIndex();
+ }
+ mOnScreenIndicators.updateWBIndicator(wbIndex);
+ boolean location = RecordLocationPreference.get(
+ prefs, mActivity.getContentResolver());
+ mOnScreenIndicators.updateLocationIndicator(location);
+ }
+
+ public void setCameraState(int state) {
+ }
+
+ 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(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 void enablePreviewThumb(boolean enabled) {
+ if (enabled) {
+ mPreviewThumb.setVisibility(View.VISIBLE);
+ } else {
+ mPreviewThumb.setVisibility(View.GONE);
+ }
+ }
+
+ public boolean removeTopLevelPopup() {
+ // Remove the top level popup or dialog box and return true if there's any
+ if (mPopup != null) {
+ dismissPopup();
+ 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() {
+ dismissPopup(true);
+ }
+
+ private void dismissPopup(boolean fullScreen) {
+ if (fullScreen) {
+ showUI();
+ mBlocker.setVisibility(View.VISIBLE);
+ }
+ setShowMenu(fullScreen);
+ if (mPopup != null) {
+ ((FrameLayout) mRootView).removeView(mPopup);
+ mPopup = null;
+ }
+ mMenu.popupDismissed();
+ }
+
+ 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();
+ ret = true;
+ }
+ onShowSwitcherPopup();
+ return ret;
+ }
+
+ protected void showPostCaptureAlert() {
+ mOnScreenIndicators.setVisibility(View.GONE);
+ mMenuButton.setVisibility(View.GONE);
+ Util.fadeIn(mReviewDoneButton);
+ mShutterButton.setVisibility(View.INVISIBLE);
+ Util.fadeIn(mReviewRetakeButton);
+ pauseFaceDetection();
+ }
+
+ protected void hidePostCaptureAlert() {
+ mOnScreenIndicators.setVisibility(View.VISIBLE);
+ mMenuButton.setVisibility(View.VISIBLE);
+ Util.fadeOut(mReviewDoneButton);
+ mShutterButton.setVisibility(View.VISIBLE);
+ Util.fadeOut(mReviewRetakeButton);
+ resumeFaceDetection();
+ }
+
+ 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);
+ dismissPopup(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);
+ }
+
+}
diff --git a/src/com/android/camera/NewPreviewGestures.java b/src/com/android/camera/NewPreviewGestures.java
new file mode 100644
index 0000000..39c4be6
--- /dev/null
+++ b/src/com/android/camera/NewPreviewGestures.java
@@ -0,0 +1,254 @@
+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.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.camera.PreviewGestures.SingleTapListener;
+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;
+
+/* NewPreviewGestures disambiguates touch events received on RenderOverlay
+ * and dispatch them to the proper recipient (i.e. zoom renderer or pie renderer).
+ * Touch events on CameraControls will be handled by framework.
+ * */
+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 RenderOverlay mOverlay;
+ private PieRenderer mPie;
+ private ZoomRenderer mZoom;
+ private MotionEvent mDown;
+ private MotionEvent mCurrent;
+ private ScaleGestureDetector mScale;
+ private int mMode;
+ private int mSlop;
+ private int mTapTimeout;
+ private boolean mEnabled;
+ private boolean mZoomOnly;
+ private int mOrientation;
+ private GestureDetector mGestureDetector;
+
+ private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public void onLongPress (MotionEvent e) {
+ // Open pie
+ if (mPie != null && !mPie.showsItems()) {
+ openPie();
+ }
+ }
+
+ @Override
+ public boolean onSingleTapUp (MotionEvent e) {
+ // Tap to focus when pie is not open
+ if (mPie == null || !mPie.showsItems()) {
+ mTapListener.onSingleTapUp(null, (int) e.getX(), (int) e.getY());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (mMode == MODE_ZOOM) return false;
+ int deltaX = (int) (e1.getX() - e2.getX());
+ int deltaY = (int) (e1.getY() - e2.getY());
+ if (deltaY > 2 * deltaX && deltaY > -2 * deltaX) {
+ // Open pie on swipe up
+ if (mPie != null && !mPie.showsItems()) {
+ openPie();
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_PIE) {
+ mMode = MODE_PIE;
+ openPie();
+ }
+ }
+ };
+
+ public interface SingleTapListener {
+ public void onSingleTapUp(View v, int x, int y);
+ }
+
+ public NewPreviewGestures(NewCameraActivity ctx, SingleTapListener tapListener,
+ ZoomRenderer zoom, PieRenderer pie) {
+ 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;
+ mGestureDetector = new GestureDetector(mGestureListener);
+ }
+
+ public void setRenderOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void setZoomOnly(boolean zoom) {
+ mZoomOnly = zoom;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public boolean dispatchTouch(MotionEvent m) {
+ if (!mEnabled) {
+ return false;
+ }
+ mCurrent = m;
+ if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
+ mMode = MODE_NONE;
+ mDown = MotionEvent.obtain(m);
+ }
+
+ // If pie is open, redirects all the touch events to pie.
+ if (mPie != null && mPie.isOpen()) {
+ return sendToPie(m);
+ }
+
+ // If pie is not open, send touch events to gesture detector and scale
+ // listener to recognize the gesture.
+ mGestureDetector.onTouchEvent(m);
+ if (mZoom != null) {
+ mScale.onTouchEvent(m);
+ if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
+ mMode = MODE_ZOOM;
+ mZoom.onScaleBegin(mScale);
+ } else if (MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
+ mZoom.onScaleEnd(mScale);
+ }
+ }
+ return true;
+ }
+
+ // 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 MotionEvent makeCancelEvent(MotionEvent m) {
+ MotionEvent c = MotionEvent.obtain(m);
+ c.setAction(MotionEvent.ACTION_CANCEL);
+ return c;
+ }
+
+ private void openPie() {
+ mGestureDetector.onTouchEvent(makeCancelEvent(mDown));
+ mScale.onTouchEvent(makeCancelEvent(mDown));
+ mOverlay.directDispatchTouch(mDown, mPie);
+ }
+
+ private boolean sendToPie(MotionEvent m) {
+ return mOverlay.directDispatchTouch(m, mPie);
+ }
+
+ // OnScaleGestureListener implementation
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mZoom.onScale(detector);
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (mPie == null || !mPie.isOpen()) {
+ mMode = MODE_ZOOM;
+ mGestureDetector.onTouchEvent(makeCancelEvent(mCurrent));
+ return mZoom.onScaleBegin(detector);
+ }
+ return false;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mZoom.onScaleEnd(detector);
+ }
+}
+
diff --git a/src/com/android/camera/NewVideoMenu.java b/src/com/android/camera/NewVideoMenu.java
new file mode 100644
index 0000000..1038628
--- /dev/null
+++ b/src/com/android/camera/NewVideoMenu.java
@@ -0,0 +1,209 @@
+/*
+ * 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 = null;
+ // white balance
+ if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+ item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+ 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.setLabel(mActivity.getResources().getString(R.string.camera_menu_settings_label));
+ 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);
+ // camera switcher
+ if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+ item = makeItem(R.drawable.ic_switch_back);
+ IconListPreference lpref = (IconListPreference) group.findPreference(
+ CameraSettings.KEY_CAMERA_ID);
+ item.setLabel(lpref.getLabel());
+ item.setImageResource(mActivity,
+ ((IconListPreference) lpref).getIconIds()
+ [lpref.findIndexOfValue(lpref.getValue())]);
+
+ final PieItem fitem = item;
+ 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]);
+ fitem.setImageResource(mActivity,
+ ((IconListPreference) pref).getIconIds()[index]);
+ fitem.setLabel(pref.getLabel());
+ mListener.onCameraPickerClicked(newCameraId);
+ }
+ }
+ });
+ mRenderer.addItem(item);
+ }
+ // flash
+ if (group.findPreference(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE) != null) {
+ item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE);
+ 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 0000000..96f0312
--- /dev/null
+++ b/src/com/android/camera/NewVideoModule.java
@@ -0,0 +1,2345 @@
+/*
+ * 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 mIsInReviewMode;
+ 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) {
+ mActivity.notifyNewMedia(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) {
+ mIsInReviewMode = false;
+ doReturnToCaller(true);
+ }
+
+ @OnClickAttr
+ public void onReviewCancelClicked(View v) {
+ mIsInReviewMode = false;
+ stopVideoRecording();
+ doReturnToCaller(false);
+ }
+
+ @Override
+ public boolean isInReviewMode() {
+ return mIsInReviewMode;
+ }
+
+ 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.notifyNewMedia(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() {
+ mIsInReviewMode = true;
+ 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();
+ }
+
+ 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 0000000..ffc1c35
--- /dev/null
+++ b/src/com/android/camera/NewVideoUI.java
@@ -0,0 +1,686 @@
+/*
+ * 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,
+ 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);
+ mRenderOverlay.setGestures(mGestures);
+ }
+ mGestures.setRenderOverlay(mRenderOverlay);
+ }
+
+ 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);
+ dismissPopup(false, true);
+ }
+
+ @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;
+ }
+
+ 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() {
+ }
+ }
+
+ 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/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index e88645d..c3d1527 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -65,6 +65,7 @@
import com.android.gallery3d.exif.ExifTag;
import com.android.gallery3d.exif.Rational;
import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.filtershow.crop.CropExtras;
import com.android.gallery3d.util.UsageStatistics;
@@ -1191,7 +1192,7 @@
newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true);
}
- Intent cropIntent = new Intent(FilterShowActivity.CROP_ACTION);
+ Intent cropIntent = new Intent(CropActivity.CROP_ACTION);
cropIntent.setData(tempUri);
cropIntent.putExtras(newExtras);
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
new file mode 100644
index 0000000..6e2ff10
--- /dev/null
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -0,0 +1,382 @@
+/*
+ * 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.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+import android.view.View;
+
+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.Comparator;
+import java.util.List;
+
+/**
+ * A FilmStrip.DataProvider 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() || id < 0) {
+ return null;
+ }
+ return mImages.get(id);
+ }
+
+ @Override
+ public void suggestDecodeSize(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.getConstantState().newDrawable());
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ mListener = listener;
+ if (mImages != null) {
+ mListener.onDataLoaded();
+ }
+ }
+
+ public void removeData(int dataID) {
+ if (dataID >= mImages.size()) return;
+ LocalData d = mImages.remove(dataID);
+ mListener.onDataRemoved(dataID, d);
+ }
+
+ private void insertData(LocalData data) {
+ if (mImages == null) {
+ mImages = new ArrayList<LocalData>();
+ }
+
+ // Since this function is mostly for adding the newest data,
+ // a simple linear search should yield the best performance over a
+ // binary search.
+ int pos = 0;
+ Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
+ for (; pos < mImages.size()
+ && comp.compare(data, mImages.get(pos)) > 0; pos++);
+ mImages.add(pos, data);
+ if (mListener != null) {
+ mListener.onDataInserted(pos, data);
+ }
+ }
+
+ public void addNewVideo(ContentResolver cr, Uri uri) {
+ Cursor c = cr.query(uri,
+ LocalData.Video.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Video.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ insertData(LocalData.Video.buildFromCursor(c));
+ }
+ }
+
+ public void addNewPhoto(ContentResolver cr, Uri uri) {
+ Cursor c = cr.query(uri,
+ LocalData.Photo.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Photo.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ insertData(LocalData.Photo.buildFromCursor(c));
+ }
+ }
+
+ 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,
+ LocalData.Photo.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Photo.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ // build up the list.
+ while (true) {
+ LocalData data = LocalData.Photo.buildFromCursor(c);
+ if (data != null) {
+ l.add(data);
+ } else {
+ Log.e(TAG, "Error loading data:"
+ + c.getString(LocalData.Photo.COL_DATA));
+ }
+ if (c.isLast()) {
+ break;
+ }
+ c.moveToNext();
+ }
+ }
+ if (c != null) {
+ c.close();
+ }
+
+ c = resolver[0].query(
+ Video.Media.EXTERNAL_CONTENT_URI,
+ LocalData.Video.QUERY_PROJECTION,
+ MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Video.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ // build up the list.
+ c.moveToFirst();
+ while (true) {
+ LocalData data = LocalData.Video.buildFromCursor(c);
+ if (data != null) {
+ l.add(data);
+ Log.v(TAG, "video data added:" + data);
+ } else {
+ Log.e(TAG, "Error loading data:"
+ + c.getString(LocalData.Video.COL_DATA));
+ }
+ if (!c.isLast()) {
+ c.moveToNext();
+ } else {
+ break;
+ }
+ }
+ }
+ if (c != null) {
+ c.close();
+ }
+
+ if (l.size() == 0) return null;
+
+ Collections.sort(l, new LocalData.NewestFirstComparator());
+ 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 1 data is 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 class CameraPreviewData implements LocalData {
+ private int width;
+ private int height;
+
+ CameraPreviewData(int w, int h) {
+ width = w;
+ height = h;
+ }
+
+ @Override
+ public long getDateTaken() {
+ // This value is used for sorting.
+ return -1;
+ }
+
+ @Override
+ public long getDateModified() {
+ // This value might be used for sorting.
+ return -1;
+ }
+
+ @Override
+ public String getTitle() {
+ return "";
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public int getType() {
+ return ImageData.TYPE_CAMERA_PREVIEW;
+ }
+
+ @Override
+ public boolean isActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public View getView(Context c, int width, int height, Drawable placeHolder) {
+ return mCameraPreviewView;
+ }
+
+ @Override
+ public void prepare() {
+ // do nothing.
+ }
+
+ @Override
+ public void recycle() {
+ // do nothing.
+ }
+ }
+
+}
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
new file mode 100644
index 0000000..9f9be8e
--- /dev/null
+++ b/src/com/android/camera/data/LocalData.java
@@ -0,0 +1,442 @@
+/*
+ * 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.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.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.ui.FilmStripView;
+
+import java.util.Comparator;
+import java.util.Date;
+
+/* An abstract interface that represents the local media data. Also implements
+ * Comparable interface so we can sort in DataAdapter.
+ */
+abstract interface LocalData extends FilmStripView.ImageData {
+ static final String TAG = "LocalData";
+
+ abstract View getView(Context c, int width, int height, Drawable placeHolder);
+ abstract long getDateTaken();
+ abstract long getDateModified();
+ abstract String getTitle();
+
+ static class NewestFirstComparator implements Comparator<LocalData> {
+
+ // Compare taken/modified date of LocalData in descent order to make
+ // newer data in the front.
+ // The negavive numbers here are always considered "bigger" than
+ // postive ones. Thus, if any one of the numbers is negative, the logic
+ // is reversed.
+ private static int compareDate(long v1, long v2) {
+ if (v1 >= 0 && v2 >= 0) {
+ return ((v1 < v2) ? 1 : ((v1 > v2) ? -1 : 0));
+ }
+ return ((v2 < v1) ? 1 : ((v2 > v1) ? -1 : 0));
+ }
+
+ @Override
+ public int compare(LocalData d1, LocalData d2) {
+ int cmp = compareDate(d1.getDateTaken(), d2.getDateTaken());
+ if (cmp == 0) {
+ cmp = compareDate(d1.getDateModified(), d2.getDateModified());
+ }
+ if (cmp == 0) {
+ cmp = d1.getTitle().compareTo(d2.getTitle());
+ }
+ return cmp;
+ }
+ }
+
+ /*
+ * A base class for all the local media files. The bitmap is loaded in background
+ * thread. Subclasses should implement their own background loading thread by
+ * subclassing BitmapLoadTask and overriding doInBackground() to return a bitmap.
+ */
+ abstract static class LocalMediaData implements LocalData {
+ protected long id;
+ protected String title;
+ protected String mimeType;
+ protected long dateTaken;
+ protected long dateModified;
+ protected String path;
+ // width and height should be adjusted according to orientation.
+ protected int width;
+ protected int height;
+
+ // true if this data has a corresponding visible view.
+ protected Boolean mUsing = false;
+
+ @Override
+ public long getDateTaken() {
+ return dateTaken;
+ }
+
+ @Override
+ public long getDateModified() {
+ return dateModified;
+ }
+
+ @Override
+ public String getTitle() {
+ return new String(title);
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public boolean isActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public View getView(Context c,
+ int decodeWidth, int decodeHeight, Drawable placeHolder) {
+ ImageView v = new ImageView(c);
+ v.setImageDrawable(placeHolder);
+
+ v.setScaleType(ImageView.ScaleType.FIT_XY);
+ BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight);
+ task.execute();
+ return v;
+ }
+
+ @Override
+ public void prepare() {
+ synchronized (mUsing) {
+ mUsing = true;
+ }
+ }
+
+ @Override
+ public void recycle() {
+ synchronized (mUsing) {
+ mUsing = false;
+ }
+ }
+
+ protected boolean isUsing() {
+ synchronized (mUsing) {
+ return mUsing;
+ }
+ }
+
+ @Override
+ public abstract int getType();
+
+ protected abstract BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight);
+
+ /*
+ * An AsyncTask class that loads the bitmap in the background thread.
+ * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )"
+ */
+ protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
+ protected ImageView mView;
+
+ protected BitmapLoadTask(ImageView v) {
+ mView = v;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (!isUsing()) return;
+ if (bitmap == null) {
+ Log.e(TAG, "Failed decoding bitmap for file:" + path);
+ return;
+ }
+ mView.setScaleType(ImageView.ScaleType.FIT_XY);
+ mView.setImageBitmap(bitmap);
+ }
+ }
+ }
+
+ static class Photo extends LocalMediaData {
+ public static final int COL_ID = 0;
+ public static final int COL_TITLE = 1;
+ public static final int COL_MIME_TYPE = 2;
+ public static final int COL_DATE_TAKEN = 3;
+ public static final int COL_DATE_MODIFIED = 4;
+ public static final int COL_DATA = 5;
+ public static final int COL_ORIENTATION = 6;
+ public static final int COL_WIDTH = 7;
+ public static final int COL_HEIGHT = 8;
+
+ 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 mSupportedAction =
+ FilmStripView.ImageData.ACTION_DEMOTE
+ | FilmStripView.ImageData.ACTION_PROMOTE;
+
+ // 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 Photo buildFromCursor(Cursor c) {
+ Photo d = new Photo();
+ 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);
+ BitmapFactory.Options opts = decodeDimension(d.path);
+ if (opts != null) {
+ d.width = opts.outWidth;
+ d.height = opts.outHeight;
+ } 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
+ public String toString() {
+ return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
+ + "," + width + "x" + height + ",orientation=" + orientation
+ + ",date=" + new Date(dateTaken);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_PHOTO;
+ }
+
+ @Override
+ public boolean isActionSupported(int action) {
+ return ((action & mSupportedAction) != 0);
+ }
+
+ @Override
+ protected BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight) {
+ return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight);
+ }
+
+ private static BitmapFactory.Options decodeDimension(String path) {
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ Bitmap b = BitmapFactory.decodeFile(path, opts);
+ if (b == null) {
+ return null;
+ }
+ return opts;
+ }
+
+ private final class PhotoBitmapLoadTask extends BitmapLoadTask {
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+
+ public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) {
+ super(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() || !isUsing()) {
+ return null;
+ }
+ b = BitmapFactory.decodeFile(path, opts);
+ if (orientation != 0) {
+ if (isCancelled() || !isUsing()) {
+ return null;
+ }
+ Matrix m = new Matrix();
+ m.setRotate((float) orientation);
+ b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ }
+ return b;
+ }
+ }
+ }
+
+ static class Video extends LocalMediaData {
+ public static final int COL_ID = 0;
+ public static final int COL_TITLE = 1;
+ public static final int COL_MIME_TYPE = 2;
+ public static final int COL_DATE_TAKEN = 3;
+ public static final int COL_DATE_MODIFIED = 4;
+ public static final int COL_DATA = 5;
+ public static final int COL_WIDTH = 6;
+ public static final int COL_HEIGHT = 7;
+
+ private static final int mSupportedActions =
+ FilmStripView.ImageData.ACTION_DEMOTE
+ | FilmStripView.ImageData.ACTION_PROMOTE
+ | FilmStripView.ImageData.ACTION_PLAY;
+
+ 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
+ };
+
+ static Video buildFromCursor(Cursor c) {
+ Video d = new Video();
+ 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
+ public String toString() {
+ return "Video:" + ",data=" + path + ",mimeType=" + mimeType
+ + "," + width + "x" + height + ",date=" + new Date(dateTaken);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_PHOTO;
+ }
+
+ @Override
+ public boolean isActionSupported(int action) {
+ return ((action & mSupportedActions) != 0);
+ }
+
+ @Override
+ protected BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight) {
+ return new VideoBitmapLoadTask(v);
+ }
+
+ private final class VideoBitmapLoadTask extends BitmapLoadTask {
+
+ public VideoBitmapLoadTask(ImageView v) {
+ super(v);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... v) {
+ if (isCancelled() || !isUsing()) {
+ return null;
+ }
+ android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(path);
+ byte[] data = retriever.getEmbeddedPicture();
+ Bitmap bitmap = null;
+ if (isCancelled() || !isUsing()) {
+ retriever.release();
+ return null;
+ }
+ if (data != null) {
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+ if (bitmap == null) {
+ bitmap = (Bitmap) retriever.getFrameAtTime();
+ }
+ retriever.release();
+ return bitmap;
+ }
+ }
+ }
+}
+
diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java
index f4dd823..2415049 100644
--- a/src/com/android/camera/ui/FaceView.java
+++ b/src/com/android/camera/ui/FaceView.java
@@ -33,13 +33,15 @@
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,12 @@
mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke));
}
+ @Override
+ 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 0000000..f870b58
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
@@ -0,0 +1,112 @@
+/*
+ * 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);
+ boolean onUp(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 void onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ mListener.onUp(event.getX(), event.getY());
+ }
+ }
+
+ 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 0000000..4d1d6e0
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -0,0 +1,1247 @@
+/*
+ * 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 com.android.gallery3d.R;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+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.view.animation.LinearInterpolator;
+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.
+ // 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;
+ // Only check for intercepting touch events within first 500ms
+ private static final int SWIPE_TIME_OUT = 500;
+
+ private Context mContext;
+ private FilmStripGestureRecognizer mGestureRecognizer;
+ private DataAdapter mDataAdapter;
+ private int mViewGap;
+ private final Rect mDrawArea = new Rect();
+
+ private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2;
+ private float mScale;
+ private MyController mController;
+ private int mCenterX = -1;
+ private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
+
+ private Listener mListener;
+
+ private MotionEvent mDown;
+ private boolean mCheckToIntercept = true;
+ private View mCameraView;
+ private ImageData mCameraData;
+ private int mSlop;
+ private TimeInterpolator mViewAnimInterpolator;
+
+ // 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 mCenterX is not adjusted with the orientation.
+ // Set this to true when onSizeChanged is called to make sure we adjust
+ // mCenterX 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 = (1 << 1);
+ public static final int ACTION_PLAY = (1 << 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);
+
+ // prepare() should be called first time before using it.
+ public void prepare();
+
+ // recycle() should be called before we nullify the reference to this
+ // data.
+ public void recycle();
+ }
+
+ 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, ImageData data);
+ public void onDataRemoved(int dataID, ImageData data);
+ }
+
+ public int getTotalNumber();
+ public View getView(Context context, int id);
+ public ImageData getImageData(int id);
+ public void suggestDecodeSize(int w, int h);
+
+ public void setListener(Listener listener);
+ }
+
+ public interface Listener {
+ public void onDataPromoted(int dataID);
+ public void onDataDemoted(int dataID);
+ }
+
+ public interface Controller {
+ public boolean isScalling();
+
+ public void fling(float velocity);
+ public void scrollTo(int position, int duration, boolean interruptible);
+ public boolean stopScrolling();
+ public boolean isScrolling();
+
+ public void lockAtCurrentView();
+ public void unlockPosition();
+
+ public void gotoCameraFullScreen();
+ public void gotoFilmStrip();
+ public void gotoFullScreen();
+ }
+
+ // 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;
+
+ public ViewInfo(int id, View v) {
+ v.setPivotX(0f);
+ v.setPivotY(0f);
+ mDataID = id;
+ mView = v;
+ mLeftPosition = -1;
+ }
+
+ public int getID() {
+ return mDataID;
+ }
+
+ public void setID(int id) {
+ mDataID = id;
+ }
+
+ public void setLeftPosition(int pos) {
+ mLeftPosition = pos;
+ }
+
+ public int getLeftPosition() {
+ return mLeftPosition;
+ }
+
+ public float getTranslationY(float scale) {
+ return mView.getTranslationY() / scale;
+ }
+
+ public float getTranslationX(float scale) {
+ return mView.getTranslationX();
+ }
+
+ public void setTranslationY(float transY, float scale) {
+ mView.setTranslationY(transY * scale);
+ }
+
+ public void setTranslationX(float transX, float scale) {
+ mView.setTranslationX(transX * scale);
+ }
+
+ public void translateXBy(float transX, float scale) {
+ mView.setTranslationX(mView.getTranslationX() + transX * scale);
+ }
+
+ public int getCenterX() {
+ return mLeftPosition + mView.getWidth() / 2;
+ }
+
+ public int getMeasuredCenterX(float scale) {
+ return mLeftPosition + (int) (mView.getMeasuredWidth() * scale / 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);
+ 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) {
+ // 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;
+ mController = new MyController(context);
+ mViewAnimInterpolator = new LinearInterpolator();
+ mGestureRecognizer =
+ new FilmStripGestureRecognizer(context, new MyGestureReceiver());
+ mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
+ }
+
+ public Controller getController() {
+ return mController;
+ }
+
+ public void setListener(Listener l) {
+ mListener = l;
+ }
+
+ public void setViewGap(int viewGap) {
+ mViewGap = viewGap;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ public boolean isAnchoredTo(int id) {
+ if (mViewInfo[mCurrentInfo].getID() == id
+ && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) {
+ 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 (mController.hasNewGeometry()) {
+ layoutChildren();
+ }
+ }
+
+ // returns [width, height] preserving image aspect ratio
+ private int[] calculateChildDimension(
+ int imageWidth, int imageHeight,
+ int boundWidth, int boundHeight) {
+
+ if (imageWidth == ImageData.SIZE_FULL
+ || imageHeight == ImageData.SIZE_FULL) {
+ imageWidth = boundWidth;
+ imageHeight = boundHeight;
+ }
+
+ int[] ret = new int[2];
+ ret[0] = boundWidth;
+ ret[1] = boundHeight;
+
+ if (imageWidth * ret[1] > ret[0] * imageHeight) {
+ ret[1] = imageHeight * ret[0] / imageWidth;
+ } else {
+ ret[0] = imageWidth * ret[1] / imageHeight;
+ }
+
+ return ret;
+ }
+
+ @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.suggestDecodeSize(boundWidth / 2, boundHeight / 2);
+ }
+
+ for (int i = 0; i < mViewInfo.length; i++) {
+ ViewInfo info = mViewInfo[i];
+ if (mViewInfo[i] == null) continue;
+
+ int id = info.getID();
+ int[] dim = calculateChildDimension(
+ mDataAdapter.getImageData(id).getWidth(),
+ mDataAdapter.getImageData(id).getHeight(),
+ boundWidth, boundHeight);
+
+ mViewInfo[i].getView().measure(
+ View.MeasureSpec.makeMeasureSpec(
+ dim[0], View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(
+ dim[1], View.MeasureSpec.EXACTLY));
+ }
+ 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) {
+ ImageData data = mDataAdapter.getImageData(dataID);
+ if (data == null) return null;
+ data.prepare();
+ View v = mDataAdapter.getView(mContext, dataID);
+ if (v == null) return null;
+ ViewInfo info = new ViewInfo(dataID, v);
+ v = info.getView();
+ if (v != mCameraView) {
+ addView(info.getView());
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ return info;
+ }
+
+ private void removeInfo(int infoID) {
+ if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) return;
+
+ ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID());
+ checkForRemoval(data, mViewInfo[infoID].getView());
+ mViewInfo[infoID] = null;
+ }
+
+ // We try to keep the one closest to the center of the screen at position mCurrentInfo.
+ private void stepIfNeeded() {
+ int nearest = findTheNearestView(mCenterX);
+ // no change made.
+ if (nearest == -1 || nearest == mCurrentInfo) return;
+
+ int adjust = nearest - mCurrentInfo;
+ if (adjust > 0) {
+ for (int k = 0; k < adjust; k++) {
+ removeInfo(k);
+ }
+ 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--) {
+ removeInfo(k);
+ }
+ 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 beyond the bound.
+ private void adjustCenterX() {
+ ViewInfo curr = mViewInfo[mCurrentInfo];
+ if (curr == null) return;
+
+ if (curr.getID() == 0 && mCenterX < curr.getCenterX()) {
+ mCenterX = curr.getCenterX();
+ if (mController.isScrolling()) {
+ mController.stopScrolling();
+ }
+ if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW
+ && !mController.isScalling()
+ && mScale != MAX_SCALE) {
+ mController.scaleTo(MAX_SCALE, DURATION_GEOMETRY_ADJUST);
+ }
+ }
+ if (curr.getID() == mDataAdapter.getTotalNumber() - 1
+ && mCenterX > curr.getCenterX()) {
+ mCenterX = curr.getCenterX();
+ if (!mController.isScrolling()) {
+ mController.stopScrolling();
+ }
+ }
+ }
+
+ private void layoutChildren() {
+ if (mAnchorPending) {
+ mCenterX = mViewInfo[mCurrentInfo].getCenterX();
+ mAnchorPending = false;
+ }
+
+ if (mController.hasNewGeometry()) {
+ mCenterX = mController.getNewPosition();
+ mScale = mController.getNewScale();
+ }
+
+ adjustCenterX();
+
+ mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, 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() - mViewGap);
+ curr.layoutIn(mDrawArea, mCenterX, 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() + mViewGap);
+ curr.layoutIn(mDrawArea, mCenterX, 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();
+ }
+
+ // Keeps the view in the view hierarchy if it's camera preview.
+ // Remove from the hierarchy otherwise.
+ private void checkForRemoval(ImageData data, View v) {
+ if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+ removeView(v);
+ data.recycle();
+ } else {
+ v.setVisibility(View.INVISIBLE);
+ if (mCameraView != null && mCameraView != v) {
+ removeView(mCameraView);
+ mCameraData = null;
+ }
+ mCameraView = v;
+ mCameraData = data;
+ }
+ }
+
+ private void slideViewBack(View v) {
+ v.animate()
+ .translationX(0)
+ .alpha(1f)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .setInterpolator(mViewAnimInterpolator)
+ .start();
+ }
+
+ private void updateRemoval(int dataID, final ImageData data) {
+ int removedInfo = findInfoByDataID(dataID);
+
+ // adjust the data id to be consistent
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null || mViewInfo[i].getID() <= dataID) continue;
+ mViewInfo[i].setID(mViewInfo[i].getID() - 1);
+ }
+ if (removedInfo == -1) return;
+
+ final View removedView = mViewInfo[removedInfo].getView();
+ final int offsetX = (int) (removedView.getMeasuredWidth() + mViewGap);
+
+ for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX);
+ }
+ }
+
+ if (removedInfo >= mCurrentInfo
+ && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) {
+ // fill the removed info by left shift when the current one or anyone on the
+ // right is removed, and there's more data on the right available.
+ for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) {
+ mViewInfo[i] = mViewInfo[i + 1];
+ }
+
+ // pull data out from the DataAdapter for the last one.
+ int curr = BUFFER_SIZE - 1;
+ int prev = curr - 1;
+ if (mViewInfo[prev] != null) {
+ mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1);
+ }
+
+ // Translate the views to their original places.
+ for (int i = removedInfo; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(offsetX, mScale);
+ }
+ }
+
+ // The end of the filmstrip might have been changed.
+ // The mCenterX might be out of the bound.
+ ViewInfo currInfo = mViewInfo[mCurrentInfo];
+ if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1
+ && mCenterX > currInfo.getCenterX()) {
+ int adjustDiff = currInfo.getCenterX() - mCenterX;
+ mCenterX = currInfo.getCenterX();
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].translateXBy(adjustDiff, mScale);
+ }
+ }
+ }
+ } else {
+ // fill the removed place by right shift
+ mCenterX -= offsetX;
+
+ for (int i = removedInfo; i > 0; i--) {
+ mViewInfo[i] = mViewInfo[i - 1];
+ }
+
+ // pull data out from the DataAdapter for the first one.
+ int curr = 0;
+ int next = curr + 1;
+ if (mViewInfo[next] != null) {
+ mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1);
+ }
+
+ // Translate the views to their original places.
+ for (int i = removedInfo; i >= 0; i--) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(-offsetX, mScale);
+ }
+ }
+ }
+
+ // Now, slide every one back.
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null
+ && mViewInfo[i].getTranslationX(mScale) != 0f) {
+ slideViewBack(mViewInfo[i].getView());
+ }
+ }
+
+ int transY = getHeight() / 8;
+ if (removedView.getTranslationY() < 0) {
+ transY = -transY;
+ }
+ removedView.animate()
+ .alpha(0f)
+ .translationYBy(transY)
+ .setInterpolator(mViewAnimInterpolator)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ checkForRemoval(data, removedView);
+ }
+ })
+ .start();
+ layoutChildren();
+ }
+
+ // returns -1 on failure.
+ private int findInfoByDataID(int dataID) {
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null
+ && mViewInfo[i].getID() == dataID) return i;
+ }
+ return -1;
+ }
+
+ private void updateInsertion(int dataID) {
+ int insertedInfo = findInfoByDataID(dataID);
+ if (insertedInfo == -1) {
+ // Not in the current info buffers. Check if it's inserted
+ // at the end.
+ if (dataID == mDataAdapter.getTotalNumber() - 1) {
+ int prev = findInfoByDataID(dataID - 1);
+ if (prev >= 0 && prev < BUFFER_SIZE - 1) {
+ // The previous data is in the buffer and we still
+ // have room for the inserted data.
+ insertedInfo = prev + 1;
+ }
+ }
+ }
+
+ // adjust the data id to be consistent
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null || mViewInfo[i].getID() < dataID) continue;
+ mViewInfo[i].setID(mViewInfo[i].getID() + 1);
+ }
+ if (insertedInfo == -1) return;
+
+ final ImageData data = mDataAdapter.getImageData(dataID);
+ int[] dim = calculateChildDimension(
+ data.getWidth(), data.getHeight(),
+ getMeasuredWidth(), getMeasuredHeight());
+ final int offsetX = dim[0] + mViewGap;
+ ViewInfo viewInfo = buildInfoFromData(dataID);
+
+ if (insertedInfo >= mCurrentInfo) {
+ if (insertedInfo == mCurrentInfo) {
+ viewInfo.setLeftPosition(mViewInfo[mCurrentInfo].getLeftPosition());
+ }
+ // Shift right to make rooms for newly inserted item.
+ removeInfo(BUFFER_SIZE - 1);
+ for (int i = BUFFER_SIZE - 1; i > insertedInfo; i--) {
+ mViewInfo[i] = mViewInfo[i - 1];
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(-offsetX, mScale);
+ slideViewBack(mViewInfo[i].getView());
+ }
+ }
+ } else {
+ // Shift left. Put the inserted data on the left instead of the found position.
+ --insertedInfo;
+ if (insertedInfo < 0) return;
+ removeInfo(0);
+ for (int i = 1; i <= insertedInfo; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(offsetX, mScale);
+ slideViewBack(mViewInfo[i].getView());
+ mViewInfo[i - 1] = mViewInfo[i];
+ }
+ }
+ }
+
+ mViewInfo[insertedInfo] = viewInfo;
+ View insertedView = mViewInfo[insertedInfo].getView();
+ insertedView.setAlpha(0f);
+ insertedView.setTranslationY(getHeight() / 8);
+ insertedView.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setInterpolator(mViewAnimInterpolator)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .start();
+ invalidate();
+ }
+
+ public void setDataAdapter(DataAdapter adapter) {
+ mDataAdapter = adapter;
+ mDataAdapter.suggestDecodeSize(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, ImageData data) {
+ if (mViewInfo[mCurrentInfo] == null) {
+ // empty now, simply do a reload.
+ reload();
+ return;
+ }
+ updateInsertion(dataID);
+ }
+
+ @Override
+ public void onDataRemoved(int dataID, ImageData data) {
+ updateRemoval(dataID, data);
+ }
+ });
+ }
+
+ public boolean isInCameraFullscreen() {
+ return (isAnchoredTo(0) && mScale == 1f
+ && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mCheckToIntercept = true;
+ mDown = MotionEvent.obtain(ev);
+ return false;
+ } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ // Do not intercept touch once child is in zoom mode
+ mCheckToIntercept = false;
+ return false;
+ } else {
+ if (!mCheckToIntercept) return false;
+ if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) return false;
+ int deltaX = (int) (ev.getX() - mDown.getX());
+ int deltaY = (int) (ev.getY() - mDown.getY());
+ if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
+ && deltaX < mSlop * (-1)) {
+ // intercept left swipe
+ if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @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)) {
+ mCenterX = -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;
+
+ mViewInfo[mCurrentInfo] = buildInfoFromData(0);
+ mViewInfo[mCurrentInfo].setLeftPosition(0);
+ if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+ // we are in camera mode by default.
+ mController.lockAtCurrentView();
+ }
+ 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();
+ }
+
+ private void promoteData(int infoID, int dataID) {
+ if (mListener != null) {
+ mListener.onDataPromoted(dataID);
+ }
+ }
+
+ private void demoteData(int infoID, int dataID) {
+ if (mListener != null) {
+ mListener.onDataDemoted(dataID);
+ }
+ }
+
+ // MyController controls all the geometry animations. It passively
+ // tells the geometry information on demand.
+ private class MyController implements
+ Controller,
+ 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 mIsPositionLocked;
+ private int mLockedViewInfo;
+
+ MyController(Context context) {
+ mScroller = new Scroller(context);
+ mHasNewPosition = false;
+ mScaleAnimator = new ValueAnimator();
+ mScaleAnimator.addUpdateListener(MyController.this);
+ mScaleAnimator.addListener(MyController.this);
+ mDecelerateInterpolator = new DecelerateInterpolator();
+ mCanStopScroll = true;
+ mHasNewScale = false;
+ }
+
+ @Override
+ public boolean isScrolling() {
+ return !mScroller.isFinished();
+ }
+
+ @Override
+ public boolean isScalling() {
+ return mScaleAnimator.isRunning();
+ }
+
+ 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) {
+ if (mViewInfo[mLockedViewInfo] == null) return mCenterX;
+ return mViewInfo[mLockedViewInfo].getCenterX();
+ }
+ if (!mHasNewPosition) return mCenterX;
+ return mScroller.getCurrX();
+ }
+
+ @Override
+ public void lockAtCurrentView() {
+ mIsPositionLocked = true;
+ mLockedViewInfo = mCurrentInfo;
+ }
+
+ @Override
+ public void unlockPosition() {
+ if (mIsPositionLocked) {
+ // only when the position is previously locked we set the current
+ // position to make it consistent.
+ if (mViewInfo[mLockedViewInfo] != null) {
+ mCenterX = mViewInfo[mLockedViewInfo].getCenterX();
+ }
+ mIsPositionLocked = false;
+ }
+ }
+
+ private int estimateMinX(int dataID, int leftPos, int viewWidth) {
+ return (leftPos - (dataID + 100) * (viewWidth + mViewGap));
+ }
+
+ private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
+ return (leftPos
+ + (mDataAdapter.getTotalNumber() - dataID + 100)
+ * (viewWidth + mViewGap));
+ }
+
+ @Override
+ public void fling(float velocityX) {
+ if (!stopScrolling() || mIsPositionLocked) return;
+ ViewInfo info = mViewInfo[mCurrentInfo];
+ if (info == null) return;
+
+ float scaledVelocityX = velocityX / mScale;
+ if (isInCameraFullscreen() && scaledVelocityX < 0) {
+ gotoFilmStrip();
+ }
+
+ int w = getWidth();
+ // Estimation of possible length on the left. To ensure the
+ // velocity doesn't become too slow eventually, we add a huge number
+ // to the estimated maximum.
+ int minX = estimateMinX(info.getID(), info.getLeftPosition(), w);
+ // Estimation of possible length on the right. Likewise, exaggerate
+ // the possible maximum too.
+ int maxX = estimateMaxX(info.getID(), info.getLeftPosition(), w);
+ mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
+
+ layoutChildren();
+ }
+
+ @Override
+ public boolean stopScrolling() {
+ if (!mCanStopScroll) return false;
+ mScroller.forceFinished(true);
+ mHasNewPosition = false;
+ return true;
+ }
+
+ private void stopScale() {
+ mScaleAnimator.cancel();
+ mHasNewScale = false;
+ }
+
+ @Override
+ public void scrollTo(int position, int duration, boolean interruptible) {
+ if (!stopScrolling() || mIsPositionLocked) return;
+ mCanStopScroll = interruptible;
+ stopScrolling();
+ mScroller.startScroll(mCenterX, 0, position - mCenterX,
+ 0, duration);
+ }
+
+ void scrollTo(int position, int duration) {
+ scrollTo(position, duration, true);
+ }
+
+ private void scaleTo(float scale, int duration) {
+ stopScale();
+ mScaleAnimator.setDuration(duration);
+ mScaleAnimator.setFloatValues(mScale, scale);
+ mScaleAnimator.setInterpolator(mDecelerateInterpolator);
+ mScaleAnimator.start();
+ mHasNewScale = true;
+ }
+
+ @Override
+ public void gotoFilmStrip() {
+ unlockPosition();
+ scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST);
+ }
+
+ @Override
+ public void gotoFullScreen() {
+ scaleTo(1f, DURATION_GEOMETRY_ADJUST);
+ }
+
+ @Override
+ public void gotoCameraFullScreen() {
+ if (mDataAdapter.getImageData(0).getType()
+ != ImageData.TYPE_CAMERA_PREVIEW) {
+ return;
+ }
+ gotoFullScreen();
+ scrollTo(
+ estimateMinX(mViewInfo[mCurrentInfo].getID(),
+ mViewInfo[mCurrentInfo].getLeftPosition(),
+ getWidth()),
+ DURATION_GEOMETRY_ADJUST, false);
+ }
+
+ @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) {
+ ViewInfo info = mViewInfo[mCurrentInfo];
+ if (info != null && mCenterX == info.getCenterX()) {
+ if (mScale == 1f) {
+ lockAtCurrentView();
+ } else if (mScale == FILM_STRIP_SCALE) {
+ unlockPosition();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator anim) {
+ }
+
+ @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) {
+ mController.stopScrolling();
+ return true;
+ }
+
+ @Override
+ public boolean onUp(float x, float y) {
+ float halfH = getHeight() / 2;
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null) continue;
+ float transY = mViewInfo[i].getTranslationY(mScale);
+ if (transY == 0) continue;
+ int id = mViewInfo[i].getID();
+
+ if (mDataAdapter.getImageData(id)
+ .isActionSupported(ImageData.ACTION_DEMOTE)
+ && transY > halfH) {
+ demoteData(i, id);
+ } else if (mDataAdapter.getImageData(id)
+ .isActionSupported(ImageData.ACTION_PROMOTE)
+ && transY < -halfH) {
+ promoteData(i, id);
+ } else {
+ // put the view back.
+ mViewInfo[i].getView().animate()
+ .translationY(0f)
+ .alpha(1f)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .start();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(float x, float y, float dx, float dy) {
+ if (Math.abs(dx) > Math.abs(dy)) {
+ int deltaX = (int) (dx / mScale);
+ if (deltaX > 0 && isInCameraFullscreen()) {
+ mController.gotoFilmStrip();
+ }
+ mCenterX += deltaX;
+ } else {
+ // Vertical part. Promote or demote.
+ //int scaledDeltaY = (int) (dy * mScale);
+ int hit = 0;
+ Rect hitRect = new Rect();
+ for (; hit < BUFFER_SIZE; hit++) {
+ if (mViewInfo[hit] == null) continue;
+ mViewInfo[hit].getView().getHitRect(hitRect);
+ if (hitRect.contains((int) x, (int) y)) break;
+ }
+ if (hit == BUFFER_SIZE) return false;
+
+ ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID());
+ float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale;
+ if (!data.isActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) {
+ transY = 0f;
+ }
+ if (!data.isActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) {
+ transY = 0f;
+ }
+ mViewInfo[hit].setTranslationY(transY, mScale);
+ }
+
+ layoutChildren();
+ return true;
+ }
+
+ @Override
+ public boolean onFling(float velocityX, float velocityY) {
+ if (Math.abs(velocityX) > Math.abs(velocityY)) {
+ mController.fling(velocityX);
+ } else {
+ // ignore horizontal fling.
+ }
+ 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.3f + scale * 0.7f;
+ 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) {
+ mController.gotoFullScreen();
+ }
+
+ if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+ if (isAnchoredTo(0)) {
+ mController.lockAtCurrentView();
+ } else {
+ mController.scrollTo(
+ mViewInfo[mCurrentInfo].getCenterX(),
+ DURATION_GEOMETRY_ADJUST, false);
+ }
+ }
+ } else {
+ // Scale down to film strip mode.
+ if (mScale == FILM_STRIP_SCALE) {
+ mController.unlockPosition();
+ } else {
+ mController.gotoFilmStrip();
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/NewCameraRootView.java b/src/com/android/camera/ui/NewCameraRootView.java
new file mode 100644
index 0000000..bf81925
--- /dev/null
+++ b/src/com/android/camera/ui/NewCameraRootView.java
@@ -0,0 +1,145 @@
+/*
+ * 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.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+
+public class NewCameraRootView extends FrameLayout {
+
+ private int mTopMargin = 0;
+ private int mBottomMargin = 0;
+ private int mLeftMargin = 0;
+ private int mRightMargin = 0;
+ private Rect mCurrentInsets;
+ 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);
+ mCurrentInsets = 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;
+
+ if (insets.bottom > 0) {
+ mOffset = insets.bottom;
+ } else if (insets.right > 0) {
+ mOffset = insets.right;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int rotation = Util.getDisplayRotation((Activity) getContext());
+ // all the layout code assumes camera device orientation to be portrait
+ // adjust rotation for landscape
+ int orientation = getResources().getConfiguration().orientation;
+ int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+ : Configuration.ORIENTATION_LANDSCAPE;
+ if (camOrientation != orientation) {
+ rotation = (rotation + 90) % 360;
+ }
+ // calculate margins
+ mLeftMargin = 0;
+ mRightMargin = 0;
+ mBottomMargin = 0;
+ mTopMargin = 0;
+ switch (rotation) {
+ case 0:
+ mBottomMargin += mOffset;
+ break;
+ case 90:
+ mRightMargin += mOffset;
+ break;
+ case 180:
+ mTopMargin += mOffset;
+ break;
+ case 270:
+ mLeftMargin += mOffset;
+ break;
+ }
+ if (mCurrentInsets != null) {
+ if (mCurrentInsets.right > 0) {
+ // navigation bar on the right
+ mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right;
+ } else {
+ // navigation bar on the bottom
+ mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom;
+ }
+ }
+ // make sure all the children are resized
+ super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin,
+ heightMeasureSpec - mTopMargin - mBottomMargin);
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ r -= l;
+ b -= t;
+ l = 0;
+ t = 0;
+ int orientation = getResources().getConfiguration().orientation;
+ // Lay out children
+ for (int i = 0; i < getChildCount(); i++) {
+ View v = getChildAt(i);
+ if (v instanceof CameraControls) {
+ // Lay out camera controls to center on the short side of the screen
+ // so that they stay in place during rotation
+ int width = v.getMeasuredWidth();
+ int height = v.getMeasuredHeight();
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ int left = (l + r - width) / 2;
+ v.layout(left, t + mTopMargin, left + width, b - mBottomMargin);
+ } else {
+ int top = (t + b - height) / 2;
+ v.layout(l + mLeftMargin, top, r - mRightMargin, top + height);
+ }
+ } else {
+ v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin);
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ //TODO: This scale check is temporary, should be removed once full screen notification
+ // is implemented
+ if (((View) getParent()).getScaleX() == 1.0f) {
+ return super.dispatchTouchEvent(ev);
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/PieMenuButton.java b/src/com/android/camera/ui/PieMenuButton.java
index e571931..0e23226 100644
--- a/src/com/android/camera/ui/PieMenuButton.java
+++ b/src/com/android/camera/ui/PieMenuButton.java
@@ -36,6 +36,7 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
+ boolean handled = super.onTouchEvent(event);
if (MotionEvent.ACTION_UP == event.getAction() && mPressed) {
// Perform a customized click as soon as the ACTION_UP event
// is received. The reason for doing this is that Framework
@@ -45,7 +46,7 @@
mReadyToClick = true;
performClick();
}
- return super.onTouchEvent(event);
+ return handled;
}
@Override
diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
index edae2be..834dc4b 100644
--- a/src/com/android/camera/ui/PieRenderer.java
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -303,6 +303,10 @@
mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
}
+ public boolean isOpen() {
+ return mState == STATE_PIE && isVisible();
+ }
+
private void fadeIn() {
mFadeIn = new LinearAnimation(0, 1);
mFadeIn.setDuration(PIE_FADE_IN_DURATION);
diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java
index ba25915..0bda10a 100644
--- a/src/com/android/camera/ui/RenderOverlay.java
+++ b/src/com/android/camera/ui/RenderOverlay.java
@@ -23,6 +23,8 @@
import android.view.View;
import android.widget.FrameLayout;
+import com.android.camera.NewPreviewGestures;
+
import java.util.ArrayList;
import java.util.List;
@@ -42,7 +44,7 @@
private RenderView mRenderView;
private List<Renderer> mClients;
-
+ private NewPreviewGestures mGestures;
// reverse list of touch clients
private List<Renderer> mTouchClients;
private int[] mPosition = new int[2];
@@ -57,6 +59,10 @@
setWillNotDraw(false);
}
+ public void setGestures(NewPreviewGestures gestures) {
+ mGestures = gestures;
+ }
+
public void addRenderer(Renderer renderer) {
mClients.add(renderer);
renderer.setOverlay(this);
@@ -83,12 +89,13 @@
@Override
public boolean dispatchTouchEvent(MotionEvent m) {
- return false;
+ if (mGestures != null) mGestures.dispatchTouch(m);
+ return true;
}
public boolean directDispatchTouch(MotionEvent m, Renderer target) {
mRenderView.setTouchTarget(target);
- boolean res = super.dispatchTouchEvent(m);
+ boolean res = mRenderView.dispatchTouchEvent(m);
mRenderView.setTouchTarget(null);
return res;
}
@@ -123,7 +130,8 @@
}
@Override
- public boolean onTouchEvent(MotionEvent evt) {
+ public boolean dispatchTouchEvent(MotionEvent evt) {
+
if (mTouchTarget != null) {
return mTouchTarget.onTouchEvent(evt);
}
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 001ce87..658abbb 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -39,7 +39,7 @@
import com.android.gallery3d.data.MediaObject;
import com.android.gallery3d.data.MediaSet;
import com.android.gallery3d.data.Path;
-import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.filtershow.crop.CropExtras;
import com.android.gallery3d.glrenderer.FadeTexture;
import com.android.gallery3d.glrenderer.GLCanvas;
@@ -318,7 +318,7 @@
Activity activity = mActivity;
if (mData.getString(Gallery.EXTRA_CROP) != null) {
Uri uri = dm.getContentUri(item.getPath());
- Intent intent = new Intent(FilterShowActivity.CROP_ACTION, uri)
+ Intent intent = new Intent(CropActivity.CROP_ACTION, uri)
.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
.putExtras(getData());
if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java
index a4f5807..9adb4e7 100644
--- a/src/com/android/gallery3d/app/CommonControllerOverlay.java
+++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java
@@ -66,6 +66,10 @@
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 00e4cd6..ce91834 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.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 @@
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 402dfcf..246346a 100644
--- a/src/com/android/gallery3d/app/TimeBar.java
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -259,4 +259,8 @@
}
}
+ public void setSeekable(boolean canSeek) {
+ mShowScrubber = canSeek;
+ }
+
}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
index 91bc772..b0a26c2 100644
--- a/src/com/android/gallery3d/app/Wallpaper.java
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -26,7 +26,7 @@
import android.view.Display;
import com.android.gallery3d.common.ApiHelper;
-import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.filtershow.crop.CropExtras;
/**
@@ -100,7 +100,7 @@
Point size = getDefaultDisplaySize(new Point());
float spotlightX = (float) size.x / width;
float spotlightY = (float) size.y / height;
- Intent request = new Intent(FilterShowActivity.CROP_ACTION)
+ Intent request = new Intent(CropActivity.CROP_ACTION)
.setDataAndType(mPickedItem, IMAGE_TYPE)
.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
.putExtra(CropExtras.KEY_OUTPUT_X, width)
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index 67ae3b5..bdcd83c 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -19,7 +19,6 @@
import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.ProgressDialog;
-import android.app.WallpaperManager;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.Intent;
@@ -36,7 +35,6 @@
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.util.DisplayMetrics;
-import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.Menu;
@@ -49,17 +47,35 @@
import android.widget.FrameLayout;
import android.widget.ShareActionProvider;
import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
-
import android.widget.Toast;
+
import com.android.gallery3d.R;
import com.android.gallery3d.data.LocalAlbum;
import com.android.gallery3d.filtershow.cache.CachingPipeline;
import com.android.gallery3d.filtershow.cache.FilteringPipeline;
import com.android.gallery3d.filtershow.cache.ImageLoader;
-import com.android.gallery3d.filtershow.category.*;
-import com.android.gallery3d.filtershow.crop.CropExtras;
-import com.android.gallery3d.filtershow.editors.*;
-import com.android.gallery3d.filtershow.filters.*;
+import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryAdapter;
+import com.android.gallery3d.filtershow.category.CategoryView;
+import com.android.gallery3d.filtershow.category.MainPanel;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.editors.EditorFlip;
+import com.android.gallery3d.filtershow.editors.EditorInfo;
+import com.android.gallery3d.filtershow.editors.EditorManager;
+import com.android.gallery3d.filtershow.editors.EditorPanel;
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
import com.android.gallery3d.filtershow.imageshow.ImageCrop;
import com.android.gallery3d.filtershow.imageshow.ImageShow;
@@ -67,24 +83,21 @@
import com.android.gallery3d.filtershow.presets.ImagePreset;
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;
import com.android.photos.data.GalleryBitmapPool;
import java.io.File;
-import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Vector;
public class FilterShowActivity extends FragmentActivity implements OnItemClickListener,
OnShareTargetSelectedListener {
- // fields for supporting crop action
- public static final String CROP_ACTION = "com.android.camera.action.CROP";
- private CropExtras mCropExtras = null;
private String mAction = "";
MasterImage mMasterImage = null;
@@ -92,7 +105,6 @@
public static final String TINY_PLANET_ACTION = "com.android.camera.action.TINY_PLANET";
public static final String LAUNCH_FULLSCREEN = "launch-fullscreen";
- public static final int MAX_BMAP_IN_INTENT = 990000;
private ImageLoader mImageLoader = null;
private ImageShow mImageShow = null;
@@ -119,6 +131,9 @@
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 +162,7 @@
setDefaultPreset();
+ extractXMPData();
processIntent();
}
@@ -167,7 +183,6 @@
public void loadEditorPanel(FilterRepresentation representation,
final Editor currentEditor) {
if (representation.getEditorId() == ImageOnlyEditor.ID) {
- currentEditor.getImageShow().select();
currentEditor.reflectCurrentFilter();
return;
}
@@ -285,9 +300,12 @@
}
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();
}
@@ -321,16 +339,10 @@
CategoryView.setMargin((int) getPixelsFromDip(8));
CategoryView.setTextSize((int) getPixelsFromDip(16));
- ImageShow.setDefaultBackgroundColor(res.getColor(R.color.background_screen));
// TODO: get those values from XML.
FramedTextButton.setTextSize((int) getPixelsFromDip(14));
FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
- ImageShow.setTextSize((int) getPixelsFromDip(12));
- ImageShow.setTextPadding((int) getPixelsFromDip(10));
- ImageShow.setOriginalTextMargin((int) getPixelsFromDip(4));
- ImageShow.setOriginalTextSize((int) getPixelsFromDip(18));
- ImageShow.setOriginalText(res.getString(R.string.original_picture_text));
Drawable curveHandle = res.getDrawable(R.drawable.camera_crop);
int curveHandleSize = (int) res.getDimension(R.dimen.crop_indicator_size);
@@ -364,8 +376,9 @@
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));
}
}
@@ -519,6 +532,11 @@
mCategoryFiltersAdapter.imageLoaded();
mLoadBitmapTask = null;
+ if (mOriginalPreset != null) {
+ MasterImage.getImage().setPreset(mOriginalPreset, true);
+ mOriginalPreset = null;
+ }
+
if (mAction == TINY_PLANET_ACTION) {
showRepresentation(mCategoryFiltersAdapter.getTinyPlanet());
}
@@ -720,7 +738,6 @@
HistoryAdapter adapter = mMasterImage.getHistory();
int position = adapter.undo();
mMasterImage.onHistoryItemClick(position);
- mImageShow.showToast("Undo");
backToMain();
invalidateViews();
return true;
@@ -729,7 +746,6 @@
HistoryAdapter adapter = mMasterImage.getHistory();
int position = adapter.redo();
mMasterImage.onHistoryItemClick(position);
- mImageShow.showToast("Redo");
invalidateViews();
return true;
}
@@ -903,9 +919,7 @@
}
public void cannotLoadImage() {
- CharSequence text = getString(R.string.cannot_load_image);
- Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
- toast.show();
+ Toast.makeText(this, R.string.cannot_load_image, Toast.LENGTH_SHORT).show();
finish();
}
@@ -942,111 +956,23 @@
}
}
- private boolean mSaveToExtraUri = false;
- private boolean mSaveAsWallpaper = false;
- private boolean mReturnAsExtra = false;
- private boolean mOutputted = false;
public void saveImage() {
- handleSpecialExitCases();
- if (!mOutputted) {
- if (mImageShow.hasModifications()) {
- // Get the name of the album, to which the image will be saved
- File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mImageLoader.getUri());
- int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
- String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
- showSavingProgress(albumName);
- mImageShow.saveImage(this, null);
- } else {
- done();
- }
- }
- }
-
- public boolean detectSpecialExitCases() {
- return mCropExtras != null && (mCropExtras.getExtraOutput() != null
- || mCropExtras.getSetAsWallpaper() || mCropExtras.getReturnData());
- }
-
- public void handleSpecialExitCases() {
- if (mCropExtras != null) {
- if (mCropExtras.getExtraOutput() != null) {
- mSaveToExtraUri = true;
- mOutputted = true;
- }
- if (mCropExtras.getSetAsWallpaper()) {
- mSaveAsWallpaper = true;
- mOutputted = true;
- }
- if (mCropExtras.getReturnData()) {
- mReturnAsExtra = true;
- mOutputted = true;
- }
- if (mOutputted) {
- mImageShow.getImagePreset().mGeoData.setUseCropExtrasFlag(true);
- showSavingProgress(null);
- mImageShow.returnFilteredResult(this);
- }
- }
- }
-
- public void onFilteredResult(Bitmap filtered) {
- Intent intent = new Intent();
- intent.putExtra(CropExtras.KEY_CROPPED_RECT, mImageShow.getImageCropBounds());
- if (mSaveToExtraUri) {
- mImageShow.saveToUri(filtered, mCropExtras.getExtraOutput(),
- mCropExtras.getOutputFormat(), this);
- }
- if (mSaveAsWallpaper) {
- setWallpaperInBackground(filtered);
- }
- if (mReturnAsExtra) {
- if (filtered != null) {
- int bmapSize = filtered.getRowBytes() * filtered.getHeight();
- /*
- * Max size of Binder transaction buffer is 1Mb, so constrain
- * Bitmap to be somewhat less than this, otherwise we get
- * TransactionTooLargeExceptions.
- */
- if (bmapSize > MAX_BMAP_IN_INTENT) {
- Log.w(LOGTAG, "Bitmap too large to be returned via intent");
- } else {
- intent.putExtra(CropExtras.KEY_DATA, filtered);
- }
- }
- }
- setResult(RESULT_OK, intent);
- if (!mSaveToExtraUri) {
+ if (mImageShow.hasModifications()) {
+ // Get the name of the album, to which the image will be saved
+ File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mImageLoader.getUri());
+ int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
+ String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
+ showSavingProgress(albumName);
+ mImageShow.saveImage(this, null);
+ } else {
done();
}
}
- void setWallpaperInBackground(final Bitmap bmap) {
- Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
- BitmapTask.Callbacks<FilterShowActivity> cb = new BitmapTask.Callbacks<FilterShowActivity>() {
- @Override
- public void onComplete(Bitmap result) {}
-
- @Override
- public void onCancel() {}
-
- @Override
- public Bitmap onExecute(FilterShowActivity param) {
- try {
- WallpaperManager.getInstance(param).setBitmap(bmap);
- } catch (IOException e) {
- Log.w(LOGTAG, "fail to set wall paper", e);
- }
- return null;
- }
- };
- (new BitmapTask<FilterShowActivity>(cb)).execute(this);
- }
public void done() {
- if (mOutputted) {
- hideSavingProgress();
- }
+ hideSavingProgress();
finish();
}
@@ -1054,4 +980,13 @@
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 8760c4a..1ea40f2 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.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 @@
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 Resources getResources() {
+ return sRS.getApplicationContext().getResources();
+ }
+
private synchronized void destroyPixelAllocations() {
if (DEBUG) {
Log.v(LOGTAG, "destroyPixelAllocations in " + getName());
@@ -167,14 +160,14 @@
}
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 @@
|| 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 @@
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 @@
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 @@
}
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 @@
return mName;
}
+ public RenderScript getRSContext() {
+ return CachingPipeline.getRenderScriptContext();
+ }
}
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index b4e98e1..7ddd9be 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -22,12 +22,10 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Rect;
-import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
@@ -36,27 +34,19 @@
import com.adobe.xmp.XMPMeta;
import com.android.gallery3d.R;
import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.exif.ExifTag;
import com.android.gallery3d.exif.ExifInterface;
import com.android.gallery3d.filtershow.FilterShowActivity;
import com.android.gallery3d.filtershow.HistoryAdapter;
-import com.android.gallery3d.filtershow.filters.FiltersManager;
import com.android.gallery3d.filtershow.imageshow.ImageShow;
import com.android.gallery3d.filtershow.imageshow.MasterImage;
import com.android.gallery3d.filtershow.presets.ImagePreset;
-import com.android.gallery3d.filtershow.tools.BitmapTask;
import com.android.gallery3d.filtershow.tools.SaveCopyTask;
-import com.android.gallery3d.util.InterruptableOutputStream;
import com.android.gallery3d.util.XmpUtilHelper;
-import java.io.ByteArrayInputStream;
-import java.io.Closeable;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.util.Vector;
import java.util.concurrent.locks.ReentrantLock;
@@ -71,8 +61,6 @@
private Bitmap mOriginalBitmapHighres = null;
private Bitmap mBackgroundBitmap = null;
- private final ZoomCache mZoomCache = new ZoomCache();
-
private int mOrientation = 0;
private HistoryAdapter mAdapter = null;
@@ -511,116 +499,6 @@
return bmap;
}
- public void returnFilteredResult(ImagePreset preset,
- final FilterShowActivity filterShowActivity) {
- BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
-
- @Override
- public void onComplete(Bitmap result) {
- filterShowActivity.onFilteredResult(result);
- }
-
- @Override
- public void onCancel() {
- }
-
- @Override
- public Bitmap onExecute(ImagePreset param) {
- if (param == null || mUri == null) {
- return null;
- }
- BitmapFactory.Options options = new BitmapFactory.Options();
- boolean noBitmap = true;
- int num_tries = 0;
- if (options.inSampleSize < 1) {
- options.inSampleSize = 1;
- }
- Bitmap bitmap = null;
- // Stopgap fix for low-memory devices.
- while (noBitmap) {
- try {
- // Try to do bitmap operations, downsample if low-memory
- bitmap = loadMutableBitmap(mContext, mUri, options);
- if (bitmap == null) {
- Log.w(LOGTAG, "Failed to save image!");
- return null;
- }
- CachingPipeline pipeline = new CachingPipeline(
- FiltersManager.getManager(), "Saving");
- bitmap = pipeline.renderFinalImage(bitmap, param);
- noBitmap = false;
- } catch (java.lang.OutOfMemoryError e) {
- // Try 5 times before failing for good.
- if (++num_tries >= 5) {
- throw e;
- }
- bitmap = null;
- System.gc();
- options.inSampleSize *= 2;
- }
- }
- return bitmap;
- }
- };
-
- (new BitmapTask<ImagePreset>(cb)).execute(preset);
- }
-
- private String getFileExtension(String requestFormat) {
- String outputFormat = (requestFormat == null)
- ? "jpg"
- : requestFormat;
- outputFormat = outputFormat.toLowerCase();
- return (outputFormat.equals("png") || outputFormat.equals("gif"))
- ? "png" // We don't support gif compression.
- : "jpg";
- }
-
- private CompressFormat convertExtensionToCompressFormat(String extension) {
- return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
- }
-
- public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
- final FilterShowActivity filterShowActivity) {
-
- OutputStream out = null;
- try {
- out = filterShowActivity.getContentResolver().openOutputStream(uri);
- } catch (FileNotFoundException e) {
- Log.w(LOGTAG, "cannot write output", e);
- out = null;
- } finally {
- if (bmap == null || out == null) {
- return;
- }
- }
-
- final InterruptableOutputStream ios = new InterruptableOutputStream(out);
-
- BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
-
- @Override
- public void onComplete(Bitmap result) {
- filterShowActivity.done();
- }
-
- @Override
- public void onCancel() {
- ios.interrupt();
- }
-
- @Override
- public Bitmap onExecute(Bitmap param) {
- CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
- param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
- Utils.closeSilently(ios);
- return null;
- }
- };
-
- (new BitmapTask<Bitmap>(cb)).execute(bmap);
- }
-
public void setAdapter(HistoryAdapter adapter) {
mAdapter = adapter;
}
diff --git a/src/com/android/gallery3d/filtershow/cache/ZoomCache.java b/src/com/android/gallery3d/filtershow/cache/ZoomCache.java
deleted file mode 100644
index dbd0751..0000000
--- a/src/com/android/gallery3d/filtershow/cache/ZoomCache.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.filtershow.cache;
-
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-
-import com.android.gallery3d.filtershow.presets.ImagePreset;
-
-public class ZoomCache {
-
- private ImagePreset mImagePreset = null;
- private Bitmap mBitmap = null;
- private Rect mBounds = null;
-
- // TODO: move the processing to a background thread...
- public Bitmap getImage(ImagePreset preset, Rect bounds) {
- if (mBounds != bounds) {
- return null;
- }
- if (mImagePreset == null) {
- return null;
- }
- if (!mImagePreset.same(preset)) {
- return null;
- }
- return mBitmap;
- }
-
- public void setImage(ImagePreset preset, Rect bounds, Bitmap bitmap) {
- mBitmap = bitmap;
- mBounds = bounds;
- mImagePreset = preset;
- }
-
- public void reset(ImagePreset imagePreset) {
- if (imagePreset == mImagePreset) {
- mBitmap = null;
- }
- }
-}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
index 072edd7..25169c2 100644
--- a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
@@ -109,5 +109,4 @@
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 df5b6ae..9d8278d 100644
--- a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
+++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
@@ -84,5 +84,4 @@
mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
}
-
}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
index 849ac60..671554f 100644
--- a/src/com/android/gallery3d/filtershow/crop/CropMath.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -196,14 +196,13 @@
float finalH = origH;
if (origA < a) {
finalH = origW / a;
+ r.top = r.centerY() - finalH / 2;
+ r.bottom = r.top + finalH;
} else {
finalW = origH * a;
+ r.left = r.centerX() - finalW / 2;
+ r.right = r.left + finalW;
}
- float centX = r.centerX();
- float centY = r.centerY();
- float hw = finalW / 2;
- float hh = finalH / 2;
- r.set(centX - hw, centY - hh, centX + hw, centY + hh);
}
/**
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 0000000..e18d310
--- /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 0000000..4e34377
--- /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/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java
index 02bbcde..5d9755c 100644
--- a/src/com/android/gallery3d/filtershow/editors/Editor.java
+++ b/src/com/android/gallery3d/filtershow/editors/Editor.java
@@ -290,8 +290,5 @@
}
public void detach() {
- if (mImageShow != null) {
- mImageShow.unselect();
- }
}
}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorPanel.java b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
index e35bc8f..d06f30f 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
@@ -102,7 +102,6 @@
mEditor = activity.getEditor(mEditorID);
if (mEditor != null) {
mEditor.setUpEditorUI(actionControl, editControl, editTitle, toggleState);
- mEditor.getImageShow().select();
mEditor.reflectCurrentFilter();
if (mEditor.useUtilityPanel()) {
mEditor.openUtilityPanel((LinearLayout) actionControl);
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index 9927a0a..4186b93 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -17,6 +17,7 @@
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 +25,14 @@
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 +40,12 @@
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,18 +55,29 @@
}
}
+ 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);
}
+ @Override
public ImageFilter getFilterForRepresentation(FilterRepresentation representation) {
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 @@
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);
@@ -99,7 +120,7 @@
filters.add(ImageFilterVibrance.class);
filters.add(ImageFilterSharpen.class);
filters.add(ImageFilterCurves.class);
- // filters.add(ImageFilterDraw.class);
+ filters.add(ImageFilterDraw.class);
filters.add(ImageFilterHue.class);
filters.add(ImageFilterSaturated.class);
filters.add(ImageFilterBwFilter.class);
@@ -145,6 +166,7 @@
FilterFxRepresentation fx = new FilterFxRepresentation(
context.getString(fxNameid[i]), drawid[i], fxNameid[i]);
representations.add(fx);
+ addRepresentation(fx);
}
}
@@ -168,8 +190,8 @@
}
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 4d0651e..368e5c0 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -31,6 +31,8 @@
private int mMaximum;
private int mDefaultValue;
private int mPreviewValue;
+ public static final String SERIAL_NAME = "Name";
+ public static final String SERIAL_VALUE = "Value";
private boolean mLogVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
public FilterBasicRepresentation(String name, int minimum, int value, int maximum) {
@@ -171,4 +173,23 @@
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 cbcae4b..a32068a 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
@@ -15,6 +15,7 @@
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 dc59b0c..9b144b9 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
@@ -49,6 +49,7 @@
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 6e2e7ea..d0ba302 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
@@ -16,12 +16,11 @@
package com.android.gallery3d.filtershow.filters;
-import android.graphics.Bitmap;
-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 +28,8 @@
public FilterFxRepresentation(String name, int bitmapResource, int nameResource) {
super(name);
+ setSerializationName(SERIALIZATION_NAME + "_" + name);
+
mBitmapResource = bitmapResource;
mNameResource = nameResource;
setFilterClass(ImageFilterFx.class);
@@ -41,6 +42,7 @@
setSupportsPartialRendering(true);
}
+ @Override
public String toString() {
return "FilterFx: " + hashCode() + " : " + getName() + " bitmap rsc: " + mBitmapResource;
}
@@ -54,6 +56,7 @@
return representation;
}
+ @Override
public synchronized void useParametersFrom(FilterRepresentation a) {
if (a instanceof FilterFxRepresentation) {
FilterFxRepresentation representation = (FilterFxRepresentation) a;
@@ -78,6 +81,7 @@
return false;
}
+ @Override
public boolean same(FilterRepresentation representation) {
if (!super.same(representation)) {
return false;
@@ -85,6 +89,7 @@
return equals(representation);
}
+ @Override
public boolean allowsMultipleInstances() {
return true;
}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
index 3f823ea..8a87841 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
@@ -28,6 +28,7 @@
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 82012b9..3bb60bb 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 @@
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 @@
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 @@
return mName;
}
+ public void setScrName(String name) {
+ mName = name;
+ }
+
public void setName(String name) {
mName = name;
}
@@ -104,6 +110,14 @@
return mName;
}
+ public void setSerializationName(String sname) {
+ mSerializationName = sname;
+ }
+
+ public String getSerializationName() {
+ return mSerializationName;
+ }
+
public void setPriority(int priority) {
mPriority = priority;
}
@@ -241,4 +255,22 @@
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;
+ }
+ }
+ }
+
+ // Override this in subclasses
+ public int getStyle() {
+ return -1;
+ }
}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
index ac5e046..48c8b38 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.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 @@
// 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 eef54ef..9827088 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
@@ -29,6 +29,7 @@
public FilterVignetteRepresentation() {
super("Vignette", -100, 50, 100);
+ setSerializationName("VIGNETTE");
setShowParameterValue(true);
setPriority(FilterRepresentation.TYPE_VIGNETTE);
setTextId(R.string.vignette);
@@ -111,4 +112,44 @@
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 0000000..710128f
--- /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 96ab84f..6481e10 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 @@
// 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;
}
@@ -69,17 +69,15 @@
public boolean supportsAllocationInput() { return false; }
public void apply(Allocation in, Allocation out) {
+ setGeneralParameters();
}
public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
// do nothing here, subclasses will implement filtering here
+ setGeneralParameters();
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 +88,11 @@
}
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;
}
@@ -105,4 +104,11 @@
public FilterEnvironment getEnvironment() {
return mEnvironment;
}
+
+ public void setGeneralParameters() {
+ // should implement in subclass which like to transport
+ // some information to other filters. (like the style setting from RetroLux
+ // and Film to FixedFrame)
+ mEnvironment.clearGeneralParameters();
+ }
}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
index a4626cd..64c48df 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
@@ -23,6 +23,7 @@
public class ImageFilterBwFilter extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "BWFILTER";
public ImageFilterBwFilter() {
mName = "BW Filter";
@@ -31,6 +32,8 @@
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 2097f0d..c8b41c2 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
@@ -21,6 +21,7 @@
import android.graphics.Bitmap;
public class ImageFilterContrast extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "CONTRAST";
public ImageFilterContrast() {
mName = "Contrast";
@@ -30,6 +31,8 @@
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 0b02fc4..ea2ff35 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.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 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 1fd9071..812ab02 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
@@ -31,6 +31,7 @@
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 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 @@
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 @@
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 46a9a29..82de2b7 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
@@ -21,7 +21,7 @@
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 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 b0b0b2d..6fdcd24 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
@@ -21,7 +21,7 @@
import android.graphics.Bitmap;
public class ImageFilterExposure extends SimpleImageFilter {
-
+ private static final String SERIALIZATION_NAME = "EXPOSURE";
public ImageFilterExposure() {
mName = "Exposure";
}
@@ -30,6 +30,7 @@
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 68e8a7c..51c6612 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -37,6 +37,11 @@
mFxBitmap = null;
}
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return null;
+ }
+
public void useRepresentation(FilterRepresentation representation) {
FilterFxRepresentation parameters = (FilterFxRepresentation) representation;
mParameters = parameters;
@@ -87,4 +92,5 @@
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 0022a9e..0725dd1 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
@@ -21,6 +21,7 @@
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 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 b1f9f73..7e6f685 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
@@ -22,6 +22,7 @@
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 @@
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 29e6d16..9381381 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
@@ -22,6 +22,7 @@
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 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 c256686..0747190 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.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 cfbb560..69d18f8 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.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 @@
}
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 @@
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 @@
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 @@
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 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 @@
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 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 @@
/**
* 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
@@ -244,6 +253,8 @@
return;
}
resetAllocations();
+ mLastInputWidth = 0;
+ mLastInputHeight = 0;
setResourcesLoaded(false);
}
}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
index 0febe49..adc74c5 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
@@ -21,7 +21,7 @@
import android.graphics.Bitmap;
public class ImageFilterSaturated extends SimpleImageFilter {
-
+ private static final String SERIALIZATION_NAME = "SATURATED";
public ImageFilterSaturated() {
mName = "Saturated";
}
@@ -31,6 +31,7 @@
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 fd67ee8..845290b 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
@@ -21,7 +21,7 @@
import android.graphics.Bitmap;
public class ImageFilterShadows extends SimpleImageFilter {
-
+ private static final String SERIALIZATION_NAME = "SHADOWS";
public ImageFilterShadows() {
mName = "Shadows";
@@ -31,6 +31,7 @@
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 76ae475..1dc2c05 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
@@ -19,7 +19,7 @@
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 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 @@
}
@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 37d5739..f265c4d 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
@@ -76,7 +76,7 @@
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 ea315d3..900fd90 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
@@ -21,7 +21,7 @@
import android.graphics.Bitmap;
public class ImageFilterVibrance extends SimpleImageFilter {
-
+ private static final String SERIALIZATION_NAME = "VIBRANCE";
public ImageFilterVibrance() {
mName = "Vibrance";
}
@@ -30,6 +30,7 @@
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 e06f544..cfe1350 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.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 @@
@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 c4c293a..84a14c9 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
@@ -22,6 +22,7 @@
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 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 e5820a8..77dbd5e 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.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.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 @@
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 GeometryMetadata() {
super("GeometryMetadata");
+ setSerializationName(SERIALIZATION_NAME);
setFilterClass(ImageFilterGeometry.class);
setEditorId(EditorCrop.ID);
setTextId(0);
@@ -492,4 +520,87 @@
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;
+ }
+ }
+ }
}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
index 8fcc028..67a093b 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
@@ -26,13 +26,11 @@
public ImageDraw(Context context, AttributeSet attrs) {
super(context, attrs);
resetParameter();
- super.setOriginalDisabled(true);
}
public ImageDraw(Context context) {
super(context);
resetParameter();
- super.setOriginalDisabled(true);
}
public void setEditor(EditorDraw editorDraw) {
@@ -97,8 +95,6 @@
}
}
- ImageFilterDraw filter = (ImageFilterDraw) getCurrentFilter();
-
if (event.getAction() == MotionEvent.ACTION_DOWN) {
calcScreenMapping();
mTmpPoint[0] = event.getX();
@@ -110,7 +106,6 @@
if (event.getAction() == MotionEvent.ACTION_MOVE) {
int historySize = event.getHistorySize();
- final int pointerCount = event.getPointerCount();
for (int h = 0; h < historySize; h++) {
int p = 0;
{
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
index 0c51b16..b077c10 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
@@ -314,11 +314,6 @@
mMode = MODES.NONE;
}
- @Override
- public boolean showTitle() {
- return false;
- }
-
public String getName() {
return "Geometry";
}
@@ -409,10 +404,6 @@
@Override
public void onDraw(Canvas canvas) {
- if (getDirtyGeometryFlag()) {
- syncLocalToMasterGeometry();
- clearDirtyGeometryFlag();
- }
Bitmap image = getFiltersOnlyImage();
if (image == null) {
invalidate();
@@ -480,7 +471,7 @@
p.setStrokeWidth(2);
canvas.drawPath(path, p);
- p.setColor(getDefaultBackgroundColor());
+ p.setColor(mBackgroundColor);
p.setAlpha(128);
p.setStyle(Paint.Style.FILL);
drawShadows(canvas, p, scaledCrop);
@@ -518,7 +509,7 @@
canvas.drawBitmap(photo, m1, p);
canvas.restore();
- p.setColor(getDefaultBackgroundColor());
+ p.setColor(mBackgroundColor);
p.setStyle(Paint.Style.FILL);
scaledCrop.offset(displayCenter[0] - scaledCrop.centerX(), displayCenter[1]
- scaledCrop.centerY());
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
index b833cf8..1b93983 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -17,6 +17,7 @@
package com.android.gallery3d.filtershow.imageshow;
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -25,8 +26,6 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
-import android.net.Uri;
-import android.os.Handler;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
@@ -36,6 +35,7 @@
import android.view.View;
import android.widget.LinearLayout;
+import com.android.gallery3d.R;
import com.android.gallery3d.filtershow.FilterShowActivity;
import com.android.gallery3d.filtershow.cache.ImageLoader;
import com.android.gallery3d.filtershow.filters.ImageFilter;
@@ -51,15 +51,12 @@
private static final boolean ENABLE_ZOOMED_COMPARISON = false;
protected Paint mPaint = new Paint();
- protected static int mTextSize = 24;
- protected static int mTextPadding = 20;
+ protected int mTextSize;
+ protected int mTextPadding;
protected ImageLoader mImageLoader = null;
- private boolean mDirtyGeometry = false;
- private Bitmap mBackgroundImage = null;
- private final boolean USE_BACKGROUND_IMAGE = false;
- private static int mBackgroundColor = Color.RED;
+ protected int mBackgroundColor;
private GestureDetector mGestureDetector = null;
private ScaleGestureDetector mScaleGestureDetector = null;
@@ -69,7 +66,6 @@
private boolean mTouchShowOriginal = false;
private long mTouchShowOriginalDate = 0;
private final long mTouchShowOriginalDelayMin = 200; // 200ms
- private final long mTouchShowOriginalDelayMax = 300; // 300ms
private int mShowOriginalDirection = 0;
private static int UNVEIL_HORIZONTAL = 1;
private static int UNVEIL_VERTICAL = 2;
@@ -78,9 +74,9 @@
private Point mTouch = new Point();
private boolean mFinishedScalingOperation = false;
- private static int mOriginalTextMargin = 8;
- private static int mOriginalTextSize = 26;
- private static String mOriginalText = "Original";
+ private int mOriginalTextMargin;
+ private int mOriginalTextSize;
+ private String mOriginalText;
private boolean mZoomIn = false;
Point mOriginalTranslation = new Point();
float mOriginalScale;
@@ -90,9 +86,6 @@
SCALE,
MOVE
}
- private String mToast = null;
- private boolean mShowToast = false;
- private boolean mImportantToast = false;
InteractionMode mInteractionMode = InteractionMode.NONE;
protected GeometryMetadata getGeometry() {
@@ -101,46 +94,10 @@
private FilterShowActivity mActivity = null;
- public static void setDefaultBackgroundColor(int value) {
- mBackgroundColor = value;
- }
-
public FilterShowActivity getActivity() {
return mActivity;
}
- public int getDefaultBackgroundColor() {
- return mBackgroundColor;
- }
-
- public static void setTextSize(int value) {
- mTextSize = value;
- }
-
- public static void setTextPadding(int value) {
- mTextPadding = value;
- }
-
- public static void setOriginalTextMargin(int value) {
- mOriginalTextMargin = value;
- }
-
- public static void setOriginalTextSize(int value) {
- mOriginalTextSize = value;
- }
-
- public static void setOriginalText(String text) {
- mOriginalText = text;
- }
-
- private final Handler mHandler = new Handler();
-
- public void select() {
- }
-
- public void unselect() {
- }
-
public boolean hasModifications() {
if (getImagePreset() == null) {
return false;
@@ -157,21 +114,30 @@
mActivity.enableSave(hasModifications());
}
- public Point getTouchPoint() {
- return mTouch;
+ public ImageShow(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setupImageShow(context);
}
public ImageShow(Context context, AttributeSet attrs) {
super(context, attrs);
+ setupImageShow(context);
- setupGestureDetector(context);
- mActivity = (FilterShowActivity) context;
- MasterImage.getImage().addObserver(this);
}
public ImageShow(Context context) {
super(context);
+ setupImageShow(context);
+ }
+ private void setupImageShow(Context context) {
+ Resources res = context.getResources();
+ mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size);
+ mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding);
+ mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin);
+ mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size);
+ mBackgroundColor = res.getColor(R.color.background_screen);
+ mOriginalText = res.getString(R.string.original_picture_text);
setupGestureDetector(context);
mActivity = (FilterShowActivity) context;
MasterImage.getImage().addObserver(this);
@@ -193,25 +159,6 @@
return MasterImage.getImage().getCurrentFilter();
}
- public void showToast(String text) {
- showToast(text, false);
- }
-
- public void showToast(String text, boolean important) {
- mToast = text;
- mShowToast = true;
- mImportantToast = important;
- invalidate();
-
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- mShowToast = false;
- invalidate();
- }
- }, 400);
- }
-
public Rect getImageBounds() {
Rect dst = new Rect();
getImagePreset().mGeoData.getPhotoBounds().roundOut(dst);
@@ -258,38 +205,10 @@
return invert;
}
- public Rect getDisplayedImageBounds() {
- return mImageBounds;
- }
-
public ImagePreset getImagePreset() {
return MasterImage.getImage().getPreset();
}
- public void drawToast(Canvas canvas) {
- if (mShowToast && mToast != null) {
- Paint paint = new Paint();
- paint.setTextSize(128);
- float textWidth = paint.measureText(mToast);
- int toastX = (int) ((getWidth() - textWidth) / 2.0f);
- int toastY = (int) (getHeight() / 3.0f);
-
- paint.setARGB(255, 0, 0, 0);
- canvas.drawText(mToast, toastX - 2, toastY - 2, paint);
- canvas.drawText(mToast, toastX - 2, toastY, paint);
- canvas.drawText(mToast, toastX, toastY - 2, paint);
- canvas.drawText(mToast, toastX + 2, toastY + 2, paint);
- canvas.drawText(mToast, toastX + 2, toastY, paint);
- canvas.drawText(mToast, toastX, toastY + 2, paint);
- if (mImportantToast) {
- paint.setARGB(255, 200, 0, 0);
- } else {
- paint.setARGB(255, 255, 255, 255);
- }
- canvas.drawText(mToast, toastX, toastY, paint);
- }
- }
-
@Override
public void onDraw(Canvas canvas) {
MasterImage.getImage().setImageShowSize(getWidth(), getHeight());
@@ -320,7 +239,6 @@
// TODO: center scale on gesture
canvas.scale(scaleFactor, scaleFactor, cx, cy);
canvas.translate(translation.x, translation.y);
- drawBackground(canvas);
drawImage(canvas, getFilteredImage(), true);
Bitmap highresPreview = MasterImage.getImage().getHighresImage();
if (highresPreview != null) {
@@ -328,17 +246,6 @@
}
canvas.restore();
- if (showTitle() && getImagePreset() != null) {
- mPaint.setARGB(200, 0, 0, 0);
- mPaint.setTextSize(mTextSize);
-
- Rect textRect = new Rect(0, 0, getWidth(), mTextSize + mTextPadding);
- canvas.drawRect(textRect, mPaint);
- mPaint.setARGB(255, 200, 200, 200);
- canvas.drawText(getImagePreset().name(), mTextPadding,
- 1.5f * mTextPadding, mPaint);
- }
-
Bitmap partialPreview = MasterImage.getImage().getPartialImage();
if (partialPreview != null) {
Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight());
@@ -353,8 +260,6 @@
canvas.restore();
canvas.restore();
-
- drawToast(canvas);
}
public void resetImageCaches(ImageShow caller) {
@@ -459,26 +364,6 @@
canvas.restore();
}
- public void drawBackground(Canvas canvas) {
- if (USE_BACKGROUND_IMAGE) {
- if (mBackgroundImage == null) {
- mBackgroundImage = mImageLoader.getBackgroundBitmap(getResources());
- }
- if (mBackgroundImage != null) {
- Rect s = new Rect(0, 0, mBackgroundImage.getWidth(),
- mBackgroundImage.getHeight());
- Rect d = new Rect(0, 0, getWidth(), getHeight());
- canvas.drawBitmap(mBackgroundImage, s, d, mPaint);
- }
- } else {
- canvas.drawARGB(0, 0, 0, 0);
- }
- }
-
- public boolean showTitle() {
- return false;
- }
-
public void setImageLoader(ImageLoader loader) {
mImageLoader = loader;
if (mImageLoader != null) {
@@ -487,18 +372,6 @@
}
}
- private void setDirtyGeometryFlag() {
- mDirtyGeometry = true;
- }
-
- protected void clearDirtyGeometryFlag() {
- mDirtyGeometry = false;
- }
-
- protected boolean getDirtyGeometryFlag() {
- return mDirtyGeometry;
- }
-
private void imageSizeChanged(Bitmap image) {
if (image == null || getImagePreset() == null)
return;
@@ -515,15 +388,8 @@
}
- public boolean updateGeometryFlags() {
- return true;
- }
-
public void updateImage() {
invalidate();
- if (!updateGeometryFlags()) {
- return;
- }
Bitmap bitmap = mImageLoader.getOriginalBitmapLarge();
if (bitmap != null) {
imageSizeChanged(bitmap);
@@ -539,26 +405,11 @@
mImageLoader.saveImage(getImagePreset(), filterShowActivity, file);
}
- public void saveToUri(Bitmap f, Uri u, String m, FilterShowActivity filterShowActivity) {
- mImageLoader.saveToUri(f, u, m, filterShowActivity);
- }
-
- public void returnFilteredResult(FilterShowActivity filterShowActivity) {
- mImageLoader.returnFilteredResult(getImagePreset(), filterShowActivity);
- }
public boolean scaleInProgress() {
return mScaleGestureDetector.isInProgress();
}
- protected boolean isOriginalDisabled() {
- return mOriginalDisabled;
- }
-
- protected void setOriginalDisabled(boolean originalDisabled) {
- mOriginalDisabled = originalDisabled;
- }
-
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
@@ -600,6 +451,7 @@
Point translation = MasterImage.getImage().getTranslation();
translation.x = (int) (originalTranslation.x + translateX);
translation.y = (int) (originalTranslation.y + translateY);
+ constrainTranslation(translation, scaleFactor);
MasterImage.getImage().setTranslation(translation);
mTouchShowOriginal = false;
} else if (enableComparison() && !mOriginalDisabled
@@ -630,11 +482,6 @@
return true;
}
- // listview stuff
- public void showOriginal(boolean show) {
- invalidate();
- }
-
@Override
public boolean onDoubleTap(MotionEvent arg0) {
mZoomIn = !mZoomIn;
@@ -644,26 +491,44 @@
}
if (scale != MasterImage.getImage().getScaleFactor()) {
MasterImage.getImage().setScaleFactor(scale);
+ float translateX = (getWidth() / 2 - arg0.getX());
+ float translateY = (getHeight() / 2 - arg0.getY());
+ Point translation = MasterImage.getImage().getTranslation();
+ translation.x = (int) (mOriginalTranslation.x + translateX);
+ translation.y = (int) (mOriginalTranslation.y + translateY);
+ constrainTranslation(translation, scale);
+ MasterImage.getImage().setTranslation(translation);
invalidate();
}
return true;
}
+ private void constrainTranslation(Point translation, float scale) {
+ float maxTranslationX = getWidth() / scale;
+ float maxTranslationY = getHeight() / scale;
+ if (Math.abs(translation.x) > maxTranslationX) {
+ translation.x = (int) (Math.signum(translation.x) *
+ maxTranslationX);
+ if (Math.abs(translation.y) > maxTranslationY) {
+ translation.y = (int) (Math.signum(translation.y) *
+ maxTranslationY);
+ }
+
+ }
+ }
+
@Override
public boolean onDoubleTapEvent(MotionEvent arg0) {
- // TODO Auto-generated method stub
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent arg0) {
- // TODO Auto-generated method stub
return false;
}
@Override
public boolean onDown(MotionEvent arg0) {
- // TODO Auto-generated method stub
return false;
}
@@ -680,23 +545,19 @@
@Override
public void onLongPress(MotionEvent arg0) {
- // TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
- // TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent arg0) {
- // TODO Auto-generated method stub
}
@Override
public boolean onSingleTapUp(MotionEvent arg0) {
- // TODO Auto-generated method stub
return false;
}
@@ -705,14 +566,12 @@
}
public void openUtilityPanel(final LinearLayout accessoryViewList) {
- // TODO Auto-generated method stub
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
MasterImage img = MasterImage.getImage();
float scaleFactor = img.getScaleFactor();
- Point pos = img.getTranslation();
scaleFactor = scaleFactor * detector.getScaleFactor();
if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) {
@@ -723,7 +582,6 @@
}
MasterImage.getImage().setScaleFactor(scaleFactor);
scaleFactor = img.getScaleFactor();
- pos = img.getTranslation();
float focusx = detector.getFocusX();
float focusy = detector.getFocusY();
float translateX = (focusx - mStartFocusX) / scaleFactor;
@@ -731,6 +589,7 @@
Point translation = MasterImage.getImage().getTranslation();
translation.x = (int) (mOriginalTranslation.x + translateX);
translation.y = (int) (mOriginalTranslation.y + translateY);
+ constrainTranslation(translation, scaleFactor);
MasterImage.getImage().setTranslation(translation);
invalidate();
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
index a0b59c0..8f490ca 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.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.*;
@@ -34,7 +35,7 @@
private static final String LOGTAG = "MasterImage";
private boolean DEBUG = false;
- private static final boolean DISABLEZOOM = true;
+ private static final boolean DISABLEZOOM = false;
private static MasterImage sMasterImage = null;
private static int sIconSeedSize = 128;
private static float sHistoryPreviewSize = 128.0f;
@@ -139,6 +140,7 @@
}
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 c454c1a..8d59c9f 100644
--- a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
+++ b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
@@ -18,11 +18,9 @@
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 @@
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;
}
@@ -48,6 +50,9 @@
private HashMap<Long, WeakReference<Bitmap>>
bitmapCach = new HashMap<Long, WeakReference<Bitmap>>();
+ private HashMap<Integer, Integer>
+ generalParameters = new HashMap<Integer, Integer>();
+
public void cache(Bitmap bitmap) {
if (bitmap == null) {
return;
@@ -98,11 +103,11 @@
return mQuality;
}
- public void setFiltersManager(FiltersManager filtersManager) {
+ public void setFiltersManager(FiltersManagerInterface filtersManager) {
mFiltersManager = filtersManager;
}
- public FiltersManager getFiltersManager() {
+ public FiltersManagerInterface getFiltersManager() {
return mFiltersManager;
}
@@ -114,6 +119,7 @@
if (filter.supportsAllocationInput()) {
filter.apply(in, out);
}
+ filter.setGeneralParameters();
filter.setEnvironment(null);
}
@@ -122,16 +128,36 @@
filter.useRepresentation(representation);
filter.setEnvironment(this);
Bitmap ret = filter.apply(bitmap, mScaleFactor, mQuality);
+ filter.setGeneralParameters();
filter.setEnvironment(null);
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;
+ }
+
+ public synchronized void clearGeneralParameters() {
+ generalParameters = null;
+ }
+
+ public synchronized Integer getGeneralParameter(int id) {
+ if (generalParameters == null || !generalParameters.containsKey(id)) {
+ return null;
+ }
+ return generalParameters.get(id);
+ }
+
+ public synchronized void setGeneralParameter(int id, int value) {
+ if (generalParameters == null) {
+ generalParameters = new HashMap<Integer, Integer>();
+ }
+
+ generalParameters.put(id, value);
}
}
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index 2a7e601..8476695 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -18,12 +18,19 @@
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.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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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;
@@ -605,4 +624,109 @@
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 0000000..05f0a1a
--- /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/BitmapTask.java b/src/com/android/gallery3d/filtershow/tools/BitmapTask.java
deleted file mode 100644
index 62801c1..0000000
--- a/src/com/android/gallery3d/filtershow/tools/BitmapTask.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.filtershow.tools;
-
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-
-/**
- * Asynchronous task filtering or doign I/O with bitmaps.
- */
-public class BitmapTask <T> extends AsyncTask<T, Void, Bitmap> {
-
- private Callbacks<T> mCallbacks;
- private static final String LOGTAG = "BitmapTask";
-
- public BitmapTask(Callbacks<T> callbacks) {
- mCallbacks = callbacks;
- }
-
- @Override
- protected Bitmap doInBackground(T... params) {
- if (params == null || mCallbacks == null) {
- return null;
- }
- return mCallbacks.onExecute(params[0]);
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (mCallbacks == null) {
- return;
- }
- mCallbacks.onComplete(result);
- }
-
- @Override
- protected void onCancelled() {
- if (mCallbacks == null) {
- return;
- }
- mCallbacks.onCancel();
- }
-
- /**
- * Callbacks for the asynchronous task.
- */
- public interface Callbacks<P> {
- void onComplete(Bitmap result);
-
- void onCancel();
-
- Bitmap onExecute(P param);
- }
-}
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
index 008cb57..2862be2 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
@@ -206,6 +206,8 @@
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 @@
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 0000000..be75f02
--- /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/filtershow/ui/ImageCurves.java b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
index f7dcad7..0bab395 100644
--- a/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
+++ b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
@@ -145,11 +145,6 @@
invalidate();
}
- @Override
- public boolean showTitle() {
- return false;
- }
-
private ImageFilterCurves curves() {
String filterName = getFilterName();
ImagePreset p = getImagePreset();
@@ -221,7 +216,6 @@
getSpline(mCurrentCurveIndex)
.draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(),
true, mDoingTouchMove);
- drawToast(canvas);
}
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
index eb81b6e..2a4c6cf 100644
--- a/src/com/android/gallery3d/gadget/WidgetConfigure.java
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -35,7 +35,7 @@
import com.android.gallery3d.data.LocalAlbum;
import com.android.gallery3d.data.MediaSet;
import com.android.gallery3d.data.Path;
-import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.filtershow.crop.CropExtras;
public class WidgetConfigure extends Activity {
@@ -149,7 +149,7 @@
int widgetHeight = Math.round(height * scale);
mPickedItem = data.getData();
- Intent request = new Intent(FilterShowActivity.CROP_ACTION, mPickedItem)
+ Intent request = new Intent(CropActivity.CROP_ACTION, mPickedItem)
.putExtra(CropExtras.KEY_OUTPUT_X, widgetWidth)
.putExtra(CropExtras.KEY_OUTPUT_Y, widgetHeight)
.putExtra(CropExtras.KEY_ASPECT_X, widgetWidth)
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
index 8f4854e..29def05 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -190,7 +190,7 @@
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 c281dd3..e2c5f51 100644
--- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -19,6 +19,7 @@
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 @@
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 @@
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 @@
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;
+ }
+
}
diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
index 1c71151..d7d52f6 100644
--- a/src/com/android/photos/BitmapRegionTileSource.java
+++ b/src/com/android/photos/BitmapRegionTileSource.java
@@ -16,66 +16,131 @@
package com.android.photos;
+import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.util.Log;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
import com.android.photos.views.TiledImageRenderer;
import java.io.IOException;
+/**
+ * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
+ * {@link BitmapRegionDecoder} to wrap a local file
+ */
public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
+ private static final String TAG = "BitmapRegionTileSource";
+
+ private static final boolean REUSE_BITMAP =
+ Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+ private static final int MAX_PREVIEW_SIZE = 1024;
+
BitmapRegionDecoder mDecoder;
+ int mWidth;
+ int mHeight;
+ int mTileSize;
+ private BasicTexture mPreview;
+ private final int mRotation;
+ // For use only by getTile
+ private Rect mWantRegion = new Rect();
+ private Rect mOverlapRegion = new Rect();
+ private BitmapFactory.Options mOptions;
+ private Canvas mCanvas;
- public BitmapRegionTileSource(String path) {
+ public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
+ mTileSize = TiledImageRenderer.suggestedTileSize(context);
+ mRotation = rotation;
try {
mDecoder = BitmapRegionDecoder.newInstance(path, true);
+ mWidth = mDecoder.getWidth();
+ mHeight = mDecoder.getHeight();
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "ctor failed", e);
}
+ mOptions = new BitmapFactory.Options();
+ mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ mOptions.inPreferQualityOverSpeed = true;
+ mOptions.inTempStorage = new byte[16 * 1024];
+ if (previewSize != 0) {
+ previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+ // Although this is the same size as the Bitmap that is likely already
+ // loaded, the lifecycle is different and interactions are on a different
+ // thread. Thus to simplify, this source will decode its own bitmap.
+ int sampleSize = (int) Math.ceil(Math.max(
+ mWidth / (float) previewSize, mHeight / (float) previewSize));
+ mOptions.inSampleSize = Math.max(sampleSize, 1);
+ Bitmap preview = mDecoder.decodeRegion(
+ new Rect(0, 0, mWidth, mHeight), mOptions);
+ if (preview.getWidth() <= MAX_PREVIEW_SIZE && preview.getHeight() <= MAX_PREVIEW_SIZE) {
+ mPreview = new BitmapTexture(preview);
+ } else {
+ Log.w(TAG, String.format(
+ "Failed to create preview of apropriate size! "
+ + " in: %dx%d, sample: %d, out: %dx%d",
+ mWidth, mHeight, sampleSize,
+ preview.getWidth(), preview.getHeight()));
+ }
+ }
}
@Override
public int getTileSize() {
- return 256;
+ return mTileSize;
}
@Override
public int getImageWidth() {
- return mDecoder.getWidth();
+ return mWidth;
}
@Override
public int getImageHeight() {
- return mDecoder.getHeight();
+ return mHeight;
+ }
+
+ @Override
+ public BasicTexture getPreview() {
+ return mPreview;
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
}
@Override
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
int tileSize = getTileSize();
- int t = tileSize << level;
+ if (!REUSE_BITMAP) {
+ return getTileWithoutReusingBitmap(level, x, y, tileSize);
+ }
- Rect wantRegion = new Rect(x, y, x + t, y + t);
+ int t = tileSize << level;
+ mWantRegion.set(x, y, x + t, y + t);
if (bitmap == null) {
bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
}
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inPreferredConfig = Bitmap.Config.ARGB_8888;
- options.inPreferQualityOverSpeed = true;
- options.inSampleSize = (1 << level);
- options.inBitmap = bitmap;
+ mOptions.inSampleSize = (1 << level);
+ mOptions.inBitmap = bitmap;
try {
- // In CropImage, we may call the decodeRegion() concurrently.
- bitmap = mDecoder.decodeRegion(wantRegion, options);
+ bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
} finally {
- if (options.inBitmap != bitmap && options.inBitmap != null) {
- options.inBitmap = null;
+ if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
+ mOptions.inBitmap = null;
}
}
@@ -84,4 +149,35 @@
}
return bitmap;
}
+
+ private Bitmap getTileWithoutReusingBitmap(
+ int level, int x, int y, int tileSize) {
+
+ int t = tileSize << level;
+ mWantRegion.set(x, y, x + t, y + t);
+
+ mOverlapRegion.set(0, 0, mWidth, mHeight);
+
+ mOptions.inSampleSize = (1 << level);
+ Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+
+ if (mWantRegion.equals(mOverlapRegion)) {
+ return bitmap;
+ }
+
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+ mCanvas.setBitmap(result);
+ mCanvas.drawBitmap(bitmap,
+ (mOverlapRegion.left - mWantRegion.left) >> level,
+ (mOverlapRegion.top - mWantRegion.top) >> level, null);
+ mCanvas.setBitmap(null);
+ return result;
+ }
}
diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java
index 50ea1ba..a376139 100644
--- a/src/com/android/photos/FullscreenViewer.java
+++ b/src/com/android/photos/FullscreenViewer.java
@@ -31,7 +31,7 @@
String path = getIntent().getData().toString();
mTextureView = new TiledImageView(this);
- mTextureView.setTileSource(new BitmapRegionTileSource(path));
+ mTextureView.setTileSource(new BitmapRegionTileSource(this, path, 0, 0), null);
setContentView(mTextureView);
}
diff --git a/src/com/android/photos/MultiChoiceManager.java b/src/com/android/photos/MultiChoiceManager.java
index f315cf9..49519ca 100644
--- a/src/com/android/photos/MultiChoiceManager.java
+++ b/src/com/android/photos/MultiChoiceManager.java
@@ -35,6 +35,7 @@
import com.android.gallery3d.app.TrimVideo;
import com.android.gallery3d.data.MediaObject;
import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.util.GalleryUtils;
import java.util.ArrayList;
@@ -239,7 +240,7 @@
case R.id.menu_crop:
intent.setDataAndType(uri, mime)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- .setAction(FilterShowActivity.CROP_ACTION)
+ .setAction(CropActivity.CROP_ACTION)
.setClass(mContext, FilterShowActivity.class);
mContext.startActivity(intent);
return;
diff --git a/src/com/android/photos/data/BitmapDecoder.java b/src/com/android/photos/data/BitmapDecoder.java
index a0ab410..c5101cd 100644
--- a/src/com/android/photos/data/BitmapDecoder.java
+++ b/src/com/android/photos/data/BitmapDecoder.java
@@ -18,6 +18,7 @@
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
import android.util.Log;
import android.util.Pools.Pool;
import android.util.Pools.SynchronizedPool;
@@ -41,60 +42,110 @@
private static final String TAG = BitmapDecoder.class.getSimpleName();
private static final int POOL_SIZE = 4;
private static final int TEMP_STORAGE_SIZE_BYTES = 16 * 1024;
- private static final int HEADER_MAX_SIZE = 16 * 1024;
+ private static final int HEADER_MAX_SIZE = 128 * 1024;
private static final Pool<BitmapFactory.Options> sOptions =
new SynchronizedPool<BitmapFactory.Options>(POOL_SIZE);
+ private interface Decoder<T> {
+ Bitmap decode(T input, BitmapFactory.Options options);
+
+ boolean decodeBounds(T input, BitmapFactory.Options options);
+ }
+
+ private static abstract class OnlyDecode<T> implements Decoder<T> {
+ @Override
+ public boolean decodeBounds(T input, BitmapFactory.Options options) {
+ decode(input, options);
+ return true;
+ }
+ }
+
+ private static final Decoder<InputStream> sStreamDecoder = new Decoder<InputStream>() {
+ @Override
+ public Bitmap decode(InputStream is, Options options) {
+ return BitmapFactory.decodeStream(is, null, options);
+ }
+
+ @Override
+ public boolean decodeBounds(InputStream is, Options options) {
+ is.mark(HEADER_MAX_SIZE);
+ BitmapFactory.decodeStream(is, null, options);
+ try {
+ is.reset();
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Could not decode stream to bitmap", e);
+ return false;
+ }
+ }
+ };
+
+ private static final Decoder<String> sFileDecoder = new OnlyDecode<String>() {
+ @Override
+ public Bitmap decode(String filePath, Options options) {
+ return BitmapFactory.decodeFile(filePath, options);
+ }
+ };
+
+ private static final Decoder<byte[]> sByteArrayDecoder = new OnlyDecode<byte[]>() {
+ @Override
+ public Bitmap decode(byte[] data, Options options) {
+ return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ }
+ };
+
+ private static <T> Bitmap delegateDecode(Decoder<T> decoder, T input) {
+ BitmapFactory.Options options = getOptions();
+ GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
+ try {
+ options.inJustDecodeBounds = true;
+ if (!decoder.decodeBounds(input, options)) {
+ return null;
+ }
+ options.inJustDecodeBounds = false;
+ Bitmap reuseBitmap = pool.get(options.outWidth, options.outHeight);
+ options.inBitmap = reuseBitmap;
+ Bitmap decodedBitmap = decoder.decode(input, options);
+ if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
+ pool.put(reuseBitmap);
+ }
+ return decodedBitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) {
+ throw e;
+ }
+ pool.put(options.inBitmap);
+ options.inBitmap = null;
+ return decoder.decode(input, options);
+ } finally {
+ options.inBitmap = null;
+ options.inJustDecodeBounds = false;
+ sOptions.release(options);
+ }
+ }
+
public static Bitmap decode(InputStream in) {
- BitmapFactory.Options opts = getOptions();
try {
if (!in.markSupported()) {
in = new BufferedInputStream(in);
}
- opts.inJustDecodeBounds = true;
- in.mark(HEADER_MAX_SIZE);
- BitmapFactory.decodeStream(in, null, opts);
- in.reset();
- opts.inJustDecodeBounds = false;
- GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
- Bitmap reuseBitmap = pool.get(opts.outWidth, opts.outHeight);
- opts.inBitmap = reuseBitmap;
- Bitmap decodedBitmap = BitmapFactory.decodeStream(in, null, opts);
- if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
- pool.put(reuseBitmap);
- }
- return decodedBitmap;
- } catch (IOException e) {
- Log.e(TAG, "Could not decode stream to bitmap", e);
- return null;
+ return delegateDecode(sStreamDecoder, in);
} finally {
Utils.closeSilently(in);
- release(opts);
}
}
- public static Bitmap decode(File in) {
- return decodeFile(in.toString());
+ public static Bitmap decode(File file) {
+ return decodeFile(file.getPath());
}
- public static Bitmap decodeFile(String in) {
- BitmapFactory.Options opts = getOptions();
- try {
- opts.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(in, opts);
- opts.inJustDecodeBounds = false;
- GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
- Bitmap reuseBitmap = pool.get(opts.outWidth, opts.outHeight);
- opts.inBitmap = reuseBitmap;
- Bitmap decodedBitmap = BitmapFactory.decodeFile(in, opts);
- if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
- pool.put(reuseBitmap);
- }
- return decodedBitmap;
- } finally {
- release(opts);
- }
+ public static Bitmap decodeFile(String path) {
+ return delegateDecode(sFileDecoder, path);
+ }
+
+ public static Bitmap decodeByteArray(byte[] data) {
+ return delegateDecode(sByteArrayDecoder, data);
}
public static void put(Bitmap bitmap) {
@@ -113,10 +164,4 @@
return opts;
}
-
- private static void release(BitmapFactory.Options opts) {
- opts.inBitmap = null;
- opts.inJustDecodeBounds = false;
- sOptions.release(opts);
- }
}
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
index aca3e4b..390a0d4 100644
--- a/src/com/android/photos/data/GalleryBitmapPool.java
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -23,9 +23,25 @@
import com.android.photos.data.SparseArrayBitmapPool.Node;
+/**
+ * Pool allowing the efficient reuse of bitmaps in order to avoid long
+ * garbage collection pauses.
+ */
public class GalleryBitmapPool {
private static final int CAPACITY_BYTES = 20971520;
+
+ // We found that Gallery uses bitmaps that are either square (for example,
+ // tiles of large images or square thumbnails), match one of the common
+ // photo aspect ratios (4x3, 3x2, or 16x9), or, less commonly, are of some
+ // other aspect ratio. Taking advantage of this information, we use 3
+ // SparseArrayBitmapPool instances to back the GalleryBitmapPool, which affords
+ // O(1) lookups for square bitmaps, and average-case - but *not* asymptotically -
+ // O(1) lookups for common photo aspect ratios and other miscellaneous aspect
+ // ratios. Beware of the pathological case where there are many bitmaps added
+ // to the pool with different non-square aspect ratios but the same width, as
+ // performance will degrade and the average case lookup will approach
+ // O(# of different aspect ratios).
private static final int POOL_INDEX_NONE = -1;
private static final int POOL_INDEX_SQUARE = 0;
private static final int POOL_INDEX_PHOTO = 1;
@@ -84,11 +100,20 @@
return POOL_INDEX_MISC;
}
+ /**
+ * @return Capacity of the pool in bytes.
+ */
public synchronized int getCapacity() {
return mCapacityBytes;
}
- public synchronized int getSize() {
+ /**
+ * @return Approximate total size in bytes of the bitmaps stored in the pool.
+ */
+ public int getSize() {
+ // Note that this only returns an approximate size, since multiple threads
+ // might be getting and putting Bitmaps from the pool and we lock at the
+ // sub-pool level to avoid unnecessary blocking.
int total = 0;
for (SparseArrayBitmapPool p : mPools) {
total += p.getSize();
@@ -96,6 +121,9 @@
return total;
}
+ /**
+ * @return Bitmap from the pool with the desired height/width or null if none available.
+ */
public Bitmap get(int width, int height) {
SparseArrayBitmapPool pool = getPoolForDimensions(width, height);
if (pool == null) {
@@ -105,6 +133,10 @@
}
}
+ /**
+ * Adds the given bitmap to the pool.
+ * @return Whether the bitmap was added to the pool.
+ */
public boolean put(Bitmap b) {
if (b == null || b.getConfig() != Bitmap.Config.ARGB_8888) {
return false;
@@ -118,6 +150,9 @@
}
}
+ /**
+ * Empty the pool, recycling all the bitmaps currently in it.
+ */
public void clear() {
for (SparseArrayBitmapPool p : mPools) {
p.clear();
diff --git a/src/com/android/photos/data/SparseArrayBitmapPool.java b/src/com/android/photos/data/SparseArrayBitmapPool.java
index 1ef9e9f..95e1026 100644
--- a/src/com/android/photos/data/SparseArrayBitmapPool.java
+++ b/src/com/android/photos/data/SparseArrayBitmapPool.java
@@ -20,10 +20,15 @@
import android.util.SparseArray;
import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+/**
+ * Bitmap pool backed by a sparse array indexing linked lists of bitmaps
+ * sharing the same width. Performance will degrade if using this to store
+ * many bitmaps with the same width but many different heights.
+ */
public class SparseArrayBitmapPool {
- private static final int BITMAPS_TO_KEEP_AFTER_UNNEEDED_HINT = 4;
private int mCapacityBytes;
private SparseArray<Node> mStore = new SparseArray<Node>();
private int mSizeBytes = 0;
@@ -34,53 +39,80 @@
protected static class Node {
Bitmap bitmap;
+
+ // Each node is part of two doubly linked lists:
+ // - A pool-level list (accessed by mPoolNodesHead and mPoolNodesTail)
+ // that is used for FIFO eviction of nodes when the pool gets full.
+ // - A bucket-level list for each index of the sparse array, so that
+ // each index can store more than one item.
Node prevInBucket;
Node nextInBucket;
Node nextInPool;
Node prevInPool;
}
+ /**
+ * @param capacityBytes Maximum capacity of the pool in bytes.
+ * @param nodePool Shared pool to use for recycling linked list nodes, or null.
+ */
public SparseArrayBitmapPool(int capacityBytes, Pool<Node> nodePool) {
mCapacityBytes = capacityBytes;
- mNodePool = nodePool;
+ if (nodePool == null) {
+ mNodePool = new SimplePool<Node>(32);
+ } else {
+ mNodePool = nodePool;
+ }
}
+ /**
+ * Set the maximum capacity of the pool, and if necessary trim it down to size.
+ */
public synchronized void setCapacity(int capacityBytes) {
mCapacityBytes = capacityBytes;
+
+ // No-op unless current size exceeds the new capacity.
freeUpCapacity(0);
}
private void freeUpCapacity(int bytesNeeded) {
int targetSize = mCapacityBytes - bytesNeeded;
+ // Repeatedly remove the oldest node until we have freed up at least bytesNeeded.
while (mPoolNodesTail != null && mSizeBytes > targetSize) {
unlinkAndRecycleNode(mPoolNodesTail, true);
}
}
private void unlinkAndRecycleNode(Node n, boolean recycleBitmap) {
- // Remove the node from its spot in its bucket
+ // Unlink the node from its sparse array bucket list.
if (n.prevInBucket != null) {
+ // This wasn't the head, update the previous node.
n.prevInBucket.nextInBucket = n.nextInBucket;
} else {
+ // This was the head of the bucket, replace it with the next node.
mStore.put(n.bitmap.getWidth(), n.nextInBucket);
}
if (n.nextInBucket != null) {
+ // This wasn't the tail, update the next node.
n.nextInBucket.prevInBucket = n.prevInBucket;
}
- // Remove the node from its spot in the list of pool nodes
+ // Unlink the node from the pool-wide list.
if (n.prevInPool != null) {
+ // This wasn't the head, update the previous node.
n.prevInPool.nextInPool = n.nextInPool;
} else {
+ // This was the head of the pool-wide list, update the head pointer.
mPoolNodesHead = n.nextInPool;
}
if (n.nextInPool != null) {
+ // This wasn't the tail, update the next node.
n.nextInPool.prevInPool = n.prevInPool;
} else {
+ // This was the tail, update the tail pointer.
mPoolNodesTail = n.prevInPool;
}
- // Recycle the node
+ // Recycle the node.
n.nextInBucket = null;
n.nextInPool = null;
n.prevInBucket = null;
@@ -91,16 +123,29 @@
mNodePool.release(n);
}
+ /**
+ * @return Capacity of the pool in bytes.
+ */
public synchronized int getCapacity() {
return mCapacityBytes;
}
+ /**
+ * @return Total size in bytes of the bitmaps stored in the pool.
+ */
public synchronized int getSize() {
return mSizeBytes;
}
+ /**
+ * @return Bitmap from the pool with the desired height/width or null if none available.
+ */
public synchronized Bitmap get(int width, int height) {
Node cur = mStore.get(width);
+
+ // Traverse the list corresponding to the width bucket in the
+ // sparse array, and unlink and return the first bitmap that
+ // also has the correct height.
while (cur != null) {
if (cur.bitmap.getHeight() == height) {
Bitmap b = cur.bitmap;
@@ -112,28 +157,43 @@
return null;
}
+ /**
+ * Adds the given bitmap to the pool.
+ * @return Whether the bitmap was added to the pool.
+ */
public synchronized boolean put(Bitmap b) {
if (b == null) {
return false;
}
+
+ // Ensure there is enough room to contain the new bitmap.
int bytes = b.getByteCount();
freeUpCapacity(bytes);
+
Node newNode = mNodePool.acquire();
if (newNode == null) {
newNode = new Node();
}
newNode.bitmap = b;
+
+ // We append to the head, and freeUpCapacity clears from the tail,
+ // resulting in FIFO eviction.
newNode.prevInBucket = null;
newNode.prevInPool = null;
newNode.nextInPool = mPoolNodesHead;
mPoolNodesHead = newNode;
+
+ // Insert the node into its appropriate bucket based on width.
int key = b.getWidth();
newNode.nextInBucket = mStore.get(key);
if (newNode.nextInBucket != null) {
+ // The bucket already had nodes, update the old head.
newNode.nextInBucket.prevInBucket = newNode;
}
mStore.put(key, newNode);
+
if (newNode.nextInPool == null) {
+ // This is the only node in the list, update the tail pointer.
mPoolNodesTail = newNode;
} else {
newNode.nextInPool.prevInPool = newNode;
@@ -142,7 +202,11 @@
return true;
}
+ /**
+ * Empty the pool, recycling all the bitmaps currently in it.
+ */
public synchronized void clear() {
+ // Clearing is equivalent to ensuring all the capacity is available.
freeUpCapacity(mCapacityBytes);
}
}
diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java
index c38f8f7..8a05051 100644
--- a/src/com/android/photos/views/BlockingGLTextureView.java
+++ b/src/com/android/photos/views/BlockingGLTextureView.java
@@ -16,7 +16,6 @@
package com.android.photos.views;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView.Renderer;
@@ -32,7 +31,9 @@
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
-
+/**
+ * A TextureView that supports blocking rendering for synchronous drawing
+ */
public class BlockingGLTextureView extends TextureView
implements SurfaceTextureListener {
@@ -90,7 +91,9 @@
protected void finalize() throws Throwable {
try {
destroy();
- } catch (Throwable t) {}
+ } catch (Throwable t) {
+ // Ignore
+ }
super.finalize();
}
@@ -135,8 +138,8 @@
}
EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
- int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
- return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+ int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+ return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
}
/**
@@ -161,7 +164,7 @@
* We can now initialize EGL for that display
*/
int[] version = new int[2];
- if(!mEgl.eglInitialize(mEglDisplay, version)) {
+ if (!mEgl.eglInitialize(mEglDisplay, version)) {
throw new RuntimeException("eglInitialize failed");
}
mEglConfig = chooseEglConfig();
@@ -251,7 +254,7 @@
* @return the EGL error code from eglSwapBuffers.
*/
public int swap() {
- if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+ if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
return mEgl.eglGetError();
}
return EGL10.EGL_SUCCESS;
@@ -368,19 +371,24 @@
exec(FINISH);
try {
join();
- } catch (InterruptedException e) {}
+ } catch (InterruptedException e) {
+ // Ignore
+ }
}
private void exec(int msgid) {
synchronized (mLock) {
if (mExecMsgId != INVALID) {
- throw new IllegalArgumentException("Message already set - multithreaded access?");
+ throw new IllegalArgumentException(
+ "Message already set - multithreaded access?");
}
mExecMsgId = msgid;
mLock.notify();
try {
mLock.wait();
- } catch (InterruptedException e) {}
+ } catch (InterruptedException e) {
+ // Ignore
+ }
}
}
@@ -415,12 +423,15 @@
while (mExecMsgId == INVALID) {
try {
mLock.wait();
- } catch (InterruptedException e) {}
+ } catch (InterruptedException e) {
+ // Ignore
+ }
}
handleMessageLocked(mExecMsgId);
mExecMsgId = INVALID;
mLock.notify();
}
+ mExecMsgId = FINISH;
}
}
}
diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
index a1f7107..c4e493b 100644
--- a/src/com/android/photos/views/TiledImageRenderer.java
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -23,14 +23,19 @@
import android.support.v4.util.LongSparseArray;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
import android.view.View;
import android.view.WindowManager;
import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.GLCanvas;
import com.android.gallery3d.glrenderer.UploadedTexture;
-import com.android.photos.data.GalleryBitmapPool;
+/**
+ * Handles laying out, decoding, and drawing of tiles in GL
+ */
public class TiledImageRenderer {
public static final int SIZE_UNKNOWN = -1;
@@ -62,12 +67,13 @@
private static final int STATE_RECYCLING = 0x20;
private static final int STATE_RECYCLED = 0x40;
- private static GalleryBitmapPool sTilePool = GalleryBitmapPool.getInstance();
+ private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
// TILE_SIZE must be 2^N
private int mTileSize;
private TileSource mModel;
+ private BasicTexture mPreview;
protected int mLevelCount; // cache the value of mScaledBitmaps.length
// The mLevel variable indicates which level of bitmap we should use.
@@ -116,22 +122,39 @@
private int mViewWidth, mViewHeight;
private View mParent;
+ /**
+ * Interface for providing tiles to a {@link TiledImageRenderer}
+ */
public static interface TileSource {
+
+ /**
+ * If the source does not care about the tile size, it should use
+ * {@link TiledImageRenderer#suggestedTileSize(Context)}
+ */
public int getTileSize();
public int getImageWidth();
public int getImageHeight();
+ public int getRotation();
- // The tile returned by this method can be specified this way: Assuming
- // the image size is (width, height), first take the intersection of (0,
- // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
- // in extending the region, we found some part of the region is outside
- // the image, those pixels are filled with black.
- //
- // If level > 0, it does the same operation on a down-scaled version of
- // the original image (down-scaled by a factor of 2^level), but (x, y)
- // still refers to the coordinate on the original image.
- //
- // The method would be called by the decoder thread.
+ /**
+ * Return a Preview image if available. This will be used as the base layer
+ * if higher res tiles are not yet available
+ */
+ public BasicTexture getPreview();
+
+ /**
+ * The tile returned by this method can be specified this way: Assuming
+ * the image size is (width, height), first take the intersection of (0,
+ * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+ * in extending the region, we found some part of the region is outside
+ * the image, those pixels are filled with black.
+ *
+ * If level > 0, it does the same operation on a down-scaled version of
+ * the original image (down-scaled by a factor of 2^level), but (x, y)
+ * still refers to the coordinate on the original image.
+ *
+ * The method would be called by the decoder thread.
+ */
public Bitmap getTile(int level, int x, int y, Bitmap reuse);
}
@@ -173,19 +196,23 @@
if (mRotation != rotation) {
mRotation = rotation;
mLayoutTiles = true;
- invalidate();
}
}
- private static int calulateLevelCount(TileSource source) {
- int levels = 1;
- int maxDim = Math.max(source.getImageWidth(), source.getImageHeight());
- int t = source.getTileSize();
- while (t < maxDim) {
- t <<= 1;
- levels++;
+ private void calculateLevelCount() {
+ if (mPreview != null) {
+ mLevelCount = Math.max(0, Utils.ceilLog2(
+ mImageWidth / (float) mPreview.getWidth()));
+ } else {
+ int levels = 1;
+ int maxDim = Math.max(mImageWidth, mImageHeight);
+ int t = mTileSize;
+ while (t < maxDim) {
+ t <<= 1;
+ levels++;
+ }
+ mLevelCount = levels;
}
- return levels;
}
public void notifyModelInvalidated() {
@@ -194,14 +221,15 @@
mImageWidth = 0;
mImageHeight = 0;
mLevelCount = 0;
+ mPreview = null;
} else {
mImageWidth = mModel.getImageWidth();
mImageHeight = mModel.getImageHeight();
- mLevelCount = calulateLevelCount(mModel);
+ mPreview = mModel.getPreview();
mTileSize = mModel.getTileSize();
+ calculateLevelCount();
}
mLayoutTiles = true;
- invalidate();
}
public void setViewSize(int width, int height) {
@@ -211,12 +239,13 @@
public void setPosition(int centerX, int centerY, float scale) {
if (mCenterX == centerX && mCenterY == centerY
- && mScale == scale) return;
+ && mScale == scale) {
+ return;
+ }
mCenterX = centerX;
mCenterY = centerY;
mScale = scale;
mLayoutTiles = true;
- invalidate();
}
// Prepare the tiles we want to use for display.
@@ -265,7 +294,9 @@
}
// If rotation is transient, don't update the tile.
- if (mRotation % 90 != 0) return;
+ if (mRotation % 90 != 0) {
+ return;
+ }
synchronized (mQueueLock) {
mDecodeQueue.clean();
@@ -305,7 +336,7 @@
mDecodeQueue.clean();
mUploadQueue.clean();
- // TODO disable decoder
+ // TODO(xx): disable decoder
int n = mActiveTiles.size();
for (int i = 0; i < n; i++) {
Tile tile = mActiveTiles.valueAt(i);
@@ -357,6 +388,7 @@
public void freeTextures() {
mLayoutTiles = true;
+ mTileDecoder.finishAndWait();
synchronized (mQueueLock) {
mUploadQueue.clean();
mDecodeQueue.clean();
@@ -375,10 +407,10 @@
mActiveTiles.clear();
mTileRange.set(0, 0, 0, 0);
- if (sTilePool != null) sTilePool.clear();
+ while (sTilePool.acquire() != null) {}
}
- public void draw(GLCanvas canvas) {
+ public boolean draw(GLCanvas canvas) {
layoutTiles();
uploadTiles(canvas);
@@ -388,7 +420,9 @@
int level = mLevel;
int rotation = mRotation;
int flags = 0;
- if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
+ if (rotation != 0) {
+ flags |= GLCanvas.SAVE_FLAG_MATRIX;
+ }
if (flags != 0) {
canvas.save(flags);
@@ -412,9 +446,15 @@
drawTile(canvas, tx, ty, level, x, y, length);
}
}
+ } else if (mPreview != null) {
+ mPreview.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
}
} finally {
- if (flags != 0) canvas.restore();
+ if (flags != 0) {
+ canvas.restore();
+ }
}
if (mRenderComplete) {
@@ -424,6 +464,7 @@
} else {
invalidate();
}
+ return mRenderComplete || mPreview != null;
}
private void uploadBackgroundTiles(GLCanvas canvas) {
@@ -437,17 +478,6 @@
}
}
- private void queueForUpload(Tile tile) {
- synchronized (mQueueLock) {
- mUploadQueue.push(tile);
- }
- invalidate();
- // TODO
-// if (mTileUploader.mActive.compareAndSet(false, true)) {
-// getGLRoot().addOnGLIdleListener(mTileUploader);
-// }
- }
-
private void queueForDecode(Tile tile) {
synchronized (mQueueLock) {
if (tile.mTileState == STATE_ACTIVATED) {
@@ -459,9 +489,11 @@
}
}
- private boolean decodeTile(Tile tile) {
+ private void decodeTile(Tile tile) {
synchronized (mQueueLock) {
- if (tile.mTileState != STATE_IN_QUEUE) return false;
+ if (tile.mTileState != STATE_IN_QUEUE) {
+ return;
+ }
tile.mTileState = STATE_DECODING;
}
boolean decodeComplete = tile.decode();
@@ -469,15 +501,19 @@
if (tile.mTileState == STATE_RECYCLING) {
tile.mTileState = STATE_RECYCLED;
if (tile.mDecodedTile != null) {
- if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
+ sTilePool.release(tile.mDecodedTile);
tile.mDecodedTile = null;
}
mRecycledQueue.push(tile);
- return false;
+ return;
}
tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
- return decodeComplete;
+ if (!decodeComplete) {
+ return;
+ }
+ mUploadQueue.push(tile);
}
+ invalidate();
}
private Tile obtainTile(int x, int y, int level) {
@@ -500,7 +536,7 @@
}
tile.mTileState = STATE_RECYCLED;
if (tile.mDecodedTile != null) {
- if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
+ sTilePool.release(tile.mDecodedTile);
tile.mDecodedTile = null;
}
mRecycledQueue.push(tile);
@@ -538,11 +574,16 @@
synchronized (mQueueLock) {
tile = mUploadQueue.pop();
}
- if (tile == null) break;
+ if (tile == null) {
+ break;
+ }
if (!tile.isContentValid()) {
- Utils.assertTrue(tile.mTileState == STATE_DECODED);
- tile.updateContent(canvas);
- --quota;
+ if (tile.mTileState == STATE_DECODED) {
+ tile.updateContent(canvas);
+ --quota;
+ } else {
+ Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
+ }
}
}
if (tile != null) {
@@ -574,7 +615,17 @@
queueForDecode(tile);
}
}
- drawTile(tile, canvas, source, target);
+ if (drawTile(tile, canvas, source, target)) {
+ return;
+ }
+ }
+ if (mPreview != null) {
+ int size = mTileSize << level;
+ float scaleX = (float) mPreview.getWidth() / mImageWidth;
+ float scaleY = (float) mPreview.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ canvas.drawTexture(mPreview, source, target);
}
}
@@ -588,7 +639,9 @@
// Parent can be divided to four quads and tile is one of the four.
Tile parent = tile.getParentTile();
- if (parent == null) return false;
+ if (parent == null) {
+ return false;
+ }
if (tile.mX == parent.mX) {
source.left /= 2f;
source.right /= 2f;
@@ -623,14 +676,17 @@
@Override
protected void onFreeBitmap(Bitmap bitmap) {
- if (sTilePool != null) sTilePool.put(bitmap);
+ sTilePool.release(bitmap);
}
boolean decode() {
// Get a tile from the original image. The tile is down-scaled
// by (1 << mTilelevel) from a region in the original image.
try {
- Bitmap reuse = sTilePool.get(mTileSize, mTileSize);
+ Bitmap reuse = sTilePool.acquire();
+ if (reuse != null && reuse.getWidth() != mTileSize) {
+ reuse = null;
+ }
mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
} catch (Throwable t) {
Log.w(TAG, "fail to decode tile", t);
@@ -676,7 +732,9 @@
}
public Tile getParentTile() {
- if (mTileLevel + 1 == mLevelCount) return null;
+ if (mTileLevel + 1 == mLevelCount) {
+ return null;
+ }
int size = mTileSize << (mTileLevel + 1);
int x = size * (mX / size);
int y = size * (mY / size);
@@ -695,17 +753,34 @@
public Tile pop() {
Tile tile = mHead;
- if (tile != null) mHead = tile.mNext;
+ if (tile != null) {
+ mHead = tile.mNext;
+ }
return tile;
}
public boolean push(Tile tile) {
+ if (contains(tile)) {
+ Log.w(TAG, "Attempting to add a tile already in the queue!");
+ return false;
+ }
boolean wasEmpty = mHead == null;
tile.mNext = mHead;
mHead = tile;
return wasEmpty;
}
+ private boolean contains(Tile tile) {
+ Tile other = mHead;
+ while (other != null) {
+ if (other == tile) {
+ return true;
+ }
+ other = other.mNext;
+ }
+ return false;
+ }
+
public void clean() {
mHead = null;
}
@@ -723,7 +798,7 @@
}
private Tile waitForTile() throws InterruptedException {
- synchronized(mQueueLock) {
+ synchronized (mQueueLock) {
while (true) {
Tile tile = mDecodeQueue.pop();
if (tile != null) {
@@ -739,11 +814,10 @@
try {
while (!isInterrupted()) {
Tile tile = waitForTile();
- if (decodeTile(tile)) {
- queueForUpload(tile);
- }
+ decodeTile(tile);
}
} catch (InterruptedException ex) {
+ // We were finished
}
}
diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java
index 6fe030d..8bc07c0 100644
--- a/src/com/android/photos/views/TiledImageView.java
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -16,29 +16,48 @@
package com.android.photos.views;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Align;
+import android.graphics.RectF;
+import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
+import android.os.Build;
import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.View;
import android.widget.FrameLayout;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.GLES20Canvas;
import com.android.photos.views.TiledImageRenderer.TileSource;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
+/**
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
+ * or {@link BlockingGLTextureView}.
+ */
+public class TiledImageView extends FrameLayout {
-public class TiledImageView extends FrameLayout implements OnScaleGestureListener {
+ private static final boolean USE_TEXTURE_VIEW = false;
+ private static final boolean IS_SUPPORTED =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+ private static final boolean USE_CHOREOGRAPHER =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
private BlockingGLTextureView mTextureView;
- private float mLastX, mLastY;
+ private GLSurfaceView mGLSurfaceView;
+ private boolean mInvalPending = false;
+ private FrameCallback mFrameCallback;
private static class ImageRendererWrapper {
// Guarded by locks
@@ -46,20 +65,19 @@
int centerX, centerY;
int rotation;
TileSource source;
+ Runnable isReadyCallback;
// GL thread only
TiledImageRenderer image;
}
- // TODO: left/right paging
- private ImageRendererWrapper mRenderers[] = new ImageRendererWrapper[1];
- private ImageRendererWrapper mFocusedRenderer;
+ private float[] mValues = new float[9];
// -------------------------
// Guarded by mLock
// -------------------------
private Object mLock = new Object();
- private ScaleGestureDetector mScaleGestureDetector;
+ private ImageRendererWrapper mRenderer;
public TiledImageView(Context context) {
this(context, null);
@@ -67,102 +85,99 @@
public TiledImageView(Context context, AttributeSet attrs) {
super(context, attrs);
- mTextureView = new BlockingGLTextureView(context);
- addView(mTextureView, new LayoutParams(
+ if (!IS_SUPPORTED) {
+ return;
+ }
+
+ mRenderer = new ImageRendererWrapper();
+ mRenderer.image = new TiledImageRenderer(this);
+ View view;
+ if (USE_TEXTURE_VIEW) {
+ mTextureView = new BlockingGLTextureView(context);
+ mTextureView.setRenderer(new TileRenderer());
+ view = mTextureView;
+ } else {
+ mGLSurfaceView = new GLSurfaceView(context);
+ mGLSurfaceView.setEGLContextClientVersion(2);
+ mGLSurfaceView.setRenderer(new TileRenderer());
+ mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ view = mGLSurfaceView;
+ }
+ addView(view, new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
- mTextureView.setRenderer(new TileRenderer());
- setTileSource(new ColoredTiles());
- mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ //setTileSource(new ColoredTiles());
}
public void destroy() {
- mTextureView.destroy();
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ mTextureView.destroy();
+ } else {
+ mGLSurfaceView.queueEvent(mFreeTextures);
+ }
}
- public void setTileSource(TileSource source) {
+ private Runnable mFreeTextures = new Runnable() {
+
+ @Override
+ public void run() {
+ mRenderer.image.freeTextures();
+ }
+ };
+
+ public void onPause() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (!USE_TEXTURE_VIEW) {
+ mGLSurfaceView.onPause();
+ }
+ }
+
+ public void onResume() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (!USE_TEXTURE_VIEW) {
+ mGLSurfaceView.onResume();
+ }
+ }
+
+ public void setTileSource(TileSource source, Runnable isReadyCallback) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
synchronized (mLock) {
- for (int i = 0; i < mRenderers.length; i++) {
- ImageRendererWrapper renderer = mRenderers[i];
- if (renderer == null) {
- renderer = mRenderers[i] = new ImageRendererWrapper();
- }
- renderer.source = source;
- renderer.centerX = renderer.source.getImageWidth() / 2;
- renderer.centerY = renderer.source.getImageHeight() / 2;
- renderer.rotation = 0;
- renderer.scale = 0;
- renderer.image = new TiledImageRenderer(this);
- updateScaleIfNecessaryLocked(renderer);
- }
+ mRenderer.source = source;
+ mRenderer.isReadyCallback = isReadyCallback;
+ mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
+ mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
+ mRenderer.rotation = source != null ? source.getRotation() : 0;
+ mRenderer.scale = 0;
+ updateScaleIfNecessaryLocked(mRenderer);
}
- mFocusedRenderer = mRenderers[0];
invalidate();
}
@Override
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- return true;
- }
-
- @Override
- public boolean onScale(ScaleGestureDetector detector) {
- // Don't need the lock because this will only fire inside of onTouchEvent
- mFocusedRenderer.scale *= detector.getScaleFactor();
- invalidate();
- return true;
- }
-
- @Override
- public void onScaleEnd(ScaleGestureDetector detector) {
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int action = event.getActionMasked();
- final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
- final int skipIndex = pointerUp ? event.getActionIndex() : -1;
-
- // Determine focal point
- float sumX = 0, sumY = 0;
- final int count = event.getPointerCount();
- for (int i = 0; i < count; i++) {
- if (skipIndex == i) continue;
- sumX += event.getX(i);
- sumY += event.getY(i);
- }
- final int div = pointerUp ? count - 1 : count;
- float x = sumX / div;
- float y = sumY / div;
-
- synchronized (mLock) {
- mScaleGestureDetector.onTouchEvent(event);
- switch (action) {
- case MotionEvent.ACTION_MOVE:
- mFocusedRenderer.centerX += (mLastX - x) / mFocusedRenderer.scale;
- mFocusedRenderer.centerY += (mLastY - y) / mFocusedRenderer.scale;
- invalidate();
- break;
- }
- }
-
- mLastX = x;
- mLastY = y;
- return true;
- }
-
- @Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
+ if (!IS_SUPPORTED) {
+ return;
+ }
synchronized (mLock) {
- for (ImageRendererWrapper renderer : mRenderers) {
- updateScaleIfNecessaryLocked(renderer);
- }
+ updateScaleIfNecessaryLocked(mRenderer);
}
}
private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
- if (renderer.scale > 0 || getWidth() == 0) return;
+ if (renderer == null || renderer.source == null
+ || renderer.scale > 0 || getWidth() == 0) {
+ return;
+ }
renderer.scale = Math.min(
(float) getWidth() / (float) renderer.source.getImageWidth(),
(float) getHeight() / (float) renderer.source.getImageHeight());
@@ -170,14 +185,93 @@
@Override
protected void dispatchDraw(Canvas canvas) {
- mTextureView.render();
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ mTextureView.render();
+ }
super.dispatchDraw(canvas);
}
+ @SuppressLint("NewApi")
+ @Override
+ public void setTranslationX(float translationX) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ super.setTranslationX(translationX);
+ }
+
@Override
public void invalidate() {
- super.invalidate();
- mTextureView.invalidate();
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ super.invalidate();
+ mTextureView.invalidate();
+ } else {
+ if (USE_CHOREOGRAPHER) {
+ invalOnVsync();
+ } else {
+ mGLSurfaceView.requestRender();
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void invalOnVsync() {
+ if (!mInvalPending) {
+ mInvalPending = true;
+ if (mFrameCallback == null) {
+ mFrameCallback = new FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ mInvalPending = false;
+ mGLSurfaceView.requestRender();
+ }
+ };
+ }
+ Choreographer.getInstance().postFrameCallback(mFrameCallback);
+ }
+ }
+
+ private RectF mTempRectF = new RectF();
+ public void positionFromMatrix(Matrix matrix) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (mRenderer.source != null) {
+ final int rotation = mRenderer.source.getRotation();
+ final boolean swap = !(rotation % 180 == 0);
+ final int width = swap ? mRenderer.source.getImageHeight()
+ : mRenderer.source.getImageWidth();
+ final int height = swap ? mRenderer.source.getImageWidth()
+ : mRenderer.source.getImageHeight();
+ mTempRectF.set(0, 0, width, height);
+ matrix.mapRect(mTempRectF);
+ matrix.getValues(mValues);
+ int cx = width / 2;
+ int cy = height / 2;
+ float scale = mValues[Matrix.MSCALE_X];
+ int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
+ int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
+ if (rotation == 90 || rotation == 180) {
+ cx += (mTempRectF.left / scale) - xoffset;
+ } else {
+ cx -= (mTempRectF.left / scale) - xoffset;
+ }
+ if (rotation == 180 || rotation == 270) {
+ cy += (mTempRectF.top / scale) - yoffset;
+ } else {
+ cy -= (mTempRectF.top / scale) - yoffset;
+ }
+ mRenderer.scale = scale;
+ mRenderer.centerX = swap ? cy : cx;
+ mRenderer.centerY = swap ? cx : cy;
+ invalidate();
+ }
}
private class TileRenderer implements Renderer {
@@ -187,37 +281,46 @@
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mCanvas = new GLES20Canvas();
- for (ImageRendererWrapper renderer : mRenderers) {
- renderer.image.setModel(renderer.source, renderer.rotation);
- }
+ BasicTexture.invalidateAllTextures();
+ mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mCanvas.setSize(width, height);
- for (ImageRendererWrapper renderer : mRenderers) {
- renderer.image.setViewSize(width, height);
- }
+ mRenderer.image.setViewSize(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
mCanvas.clearBuffer();
+ Runnable readyCallback;
synchronized (mLock) {
- for (ImageRendererWrapper renderer : mRenderers) {
- renderer.image.setModel(renderer.source, renderer.rotation);
- renderer.image.setPosition(renderer.centerX, renderer.centerY, renderer.scale);
- }
+ readyCallback = mRenderer.isReadyCallback;
+ mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+ mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
+ mRenderer.scale);
}
- for (ImageRendererWrapper renderer : mRenderers) {
- renderer.image.draw(mCanvas);
+ boolean complete = mRenderer.image.draw(mCanvas);
+ if (complete && readyCallback != null) {
+ synchronized (mLock) {
+ // Make sure we don't trample on a newly set callback/source
+ // if it changed while we were rendering
+ if (mRenderer.isReadyCallback == readyCallback) {
+ mRenderer.isReadyCallback = null;
+ }
+ }
+ if (readyCallback != null) {
+ post(readyCallback);
+ }
}
}
}
+ @SuppressWarnings("unused")
private static class ColoredTiles implements TileSource {
- private static int[] COLORS = new int[] {
+ private static final int[] COLORS = new int[] {
Color.RED,
Color.BLUE,
Color.YELLOW,
@@ -246,6 +349,11 @@
}
@Override
+ public int getRotation() {
+ return 0;
+ }
+
+ @Override
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
int tileSize = getTileSize();
if (bitmap == null) {
@@ -265,5 +373,10 @@
mCanvas.setBitmap(null);
return bitmap;
}
+
+ @Override
+ public BasicTexture getPreview() {
+ return null;
+ }
}
}