diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java b/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java new file mode 100644 index 00000000..543e4b27 --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java @@ -0,0 +1,52 @@ +package net.sourceforge.opencamera.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class HDRTests { + /** Tests for HDR algorithm - only need to run on a single device + * Should manually look over the images dumped onto DCIM/ + * To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/ + * folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes + * time to transfer to the device everytime we run the tests. + */ + public static Test suite() { + TestSuite suite = new TestSuite(MainTests.class.getName()); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR1")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR3")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR4")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR5")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR6")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR7")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR8")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR9")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR10")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR11")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR12")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR13")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR14")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR15")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR16")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR17")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR18")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR19")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR20")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR21")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR22")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR23")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR24")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR25")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR26")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR27")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR28")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR29")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR30")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR31")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR32")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR33")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR34")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR35")); + return suite; + } +} diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java new file mode 100644 index 00000000..8c8a075c --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -0,0 +1,7906 @@ +package net.sourceforge.opencamera.test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import net.sourceforge.opencamera.MainActivity; +import net.sourceforge.opencamera.PreferenceKeys; +import net.sourceforge.opencamera.SaveLocationHistory; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.Preview.Preview; +import net.sourceforge.opencamera.UI.FolderChooserDialog; +import net.sourceforge.opencamera.UI.PopupView; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.SharedPreferences; +//import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +//import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.ExifInterface; +import android.media.MediaScannerConnection; +import android.os.Build; +import android.os.Environment; +//import android.os.Environment; +import android.preference.PreferenceManager; +import android.test.ActivityInstrumentationTestCase2; +import android.test.TouchUtils; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.ZoomControls; + +public class MainActivityTest extends ActivityInstrumentationTestCase2 { + private static final String TAG = "MainActivityTest"; + private MainActivity mActivity = null; + private Preview mPreview = null; + + @SuppressWarnings("deprecation") + public MainActivityTest() { + super("net.sourceforge.opencamera", MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + Log.d(TAG, "setUp"); + super.setUp(); + + setActivityInitialTouchMode(false); + + // use getTargetContext() as we haven't started the activity yet (and don't want to, as we want to set prefs before starting) + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this.getInstrumentation().getTargetContext()); + SharedPreferences.Editor editor = settings.edit(); + editor.clear(); + //editor.putBoolean(PreferenceKeys.getUseCamera2PreferenceKey(), true); // uncomment to test Camera2 API + editor.apply(); + + Intent intent = new Intent(); + intent.putExtra("test_project", true); + setActivityIntent(intent); + mActivity = getActivity(); + mPreview = mActivity.getPreview(); + + //restart(); // no longer need to restart, as we reset prefs before starting up; not restarting makes tests run faster! + + //Camera camera = mPreview.getCamera(); + /*mSpinner = (Spinner) mActivity.findViewById( + com.android.example.spinner.R.id.Spinner01 + );*/ + + //mPlanetData = mSpinner.getAdapter(); + } + + @Override + protected void tearDown() throws Exception { + Log.d(TAG, "tearDown"); + + assertTrue( mPreview.getCameraController() == null || mPreview.getCameraController().count_camera_parameters_exception == 0 ); + assertTrue( mPreview.getCameraController() == null || mPreview.getCameraController().count_precapture_timeout == 0 ); + + // reset back to defaults + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.clear(); + editor.apply(); + + super.tearDown(); + } + + public void testPreConditions() { + assertTrue(mPreview != null); + //assertTrue(mPreview.getCamera() != null); + //assertTrue(mCamera != null); + //assertTrue(mSpinner.getOnItemSelectedListener() != null); + //assertTrue(mPlanetData != null); + //assertEquals(mPlanetData.getCount(),ADAPTER_COUNT); + } + + private void restart() { + Log.d(TAG, "restart"); + mActivity.finish(); + setActivity(null); + Log.d(TAG, "now starting"); + mActivity = getActivity(); + mPreview = mActivity.getPreview(); + Log.d(TAG, "restart done"); + } + + private void pauseAndResume() { + Log.d(TAG, "pauseAndResume"); + // onResume has code that must run on UI thread + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "pause..."); + getInstrumentation().callActivityOnPause(mActivity); + Log.d(TAG, "resume..."); + getInstrumentation().callActivityOnResume(mActivity); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + } + + private void updateForSettings() { + Log.d(TAG, "updateForSettings"); + // updateForSettings has code that must run on UI thread + mActivity.runOnUiThread(new Runnable() { + public void run() { + mActivity.updateForSettings(); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + } + + private void clickView(final View view) { + // TouchUtils.clickView doesn't work properly if phone held in portrait mode! + //TouchUtils.clickView(MainActivityTest.this, view); + Log.d(TAG, "clickView: "+ view); + assertTrue(view.getVisibility() == View.VISIBLE); + mActivity.runOnUiThread(new Runnable() { + public void run() { + assertTrue(view.performClick()); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + } + + private void switchToFlashValue(String required_flash_value) { + if( mPreview.supportsFlash() ) { + String flash_value = mPreview.getCurrentFlashValue(); + Log.d(TAG, "start flash_value: "+ flash_value); + Log.d(TAG, "required_flash_value: "+ required_flash_value); + if( !flash_value.equals(required_flash_value) ) { + assertFalse( mActivity.popupIsOpen() ); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + clickView(popupButton); + Log.d(TAG, "wait for popup to open"); + while( !mActivity.popupIsOpen() ) { + } + Log.d(TAG, "popup is now open"); + View currentFlashButton = mActivity.getPopupButton("TEST_FLASH_" + flash_value); + assertTrue(currentFlashButton != null); + assertTrue(currentFlashButton.getAlpha() == PopupView.ALPHA_BUTTON_SELECTED); + View flashButton = mActivity.getPopupButton("TEST_FLASH_" + required_flash_value); + assertTrue(flashButton != null); + assertTrue(flashButton.getAlpha() == PopupView.ALPHA_BUTTON); + clickView(flashButton); + flash_value = mPreview.getCurrentFlashValue(); + Log.d(TAG, "changed flash_value to: "+ flash_value); + } + assertTrue(flash_value.equals(required_flash_value)); + String controller_flash_value = mPreview.getCameraController().getFlashValue(); + Log.d(TAG, "controller_flash_value: "+ controller_flash_value); + if( flash_value.equals("flash_frontscreen_auto") || flash_value.equals("flash_frontscreen_on") ) { + // for frontscreen flash, the controller flash value will be "" (due to real flash not supported) - although on Galaxy Nexus this is "flash_off" due to parameters.getFlashMode() returning Camera.Parameters.FLASH_MODE_OFF + assertTrue(controller_flash_value.equals("") || controller_flash_value.equals("flash_off")); + } + else { + String expected_flash_value = flash_value; + Log.d(TAG, "expected_flash_value: "+ expected_flash_value); + assertTrue(expected_flash_value.equals( controller_flash_value )); + } + } + } + + private void switchToFocusValue(String required_focus_value) { + Log.d(TAG, "switchToFocusValue: "+ required_focus_value); + if( mPreview.supportsFocus() ) { + String focus_value = mPreview.getCurrentFocusValue(); + Log.d(TAG, "start focus_value: "+ focus_value); + if( !focus_value.equals(required_focus_value) ) { + assertFalse( mActivity.popupIsOpen() ); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + View focusButton = mActivity.getPopupButton("TEST_FOCUS_" + required_focus_value); + assertTrue(focusButton != null); + clickView(focusButton); + focus_value = mPreview.getCurrentFocusValue(); + Log.d(TAG, "changed focus_value to: "+ focus_value); + } + assertTrue(focus_value.equals(required_focus_value)); + String actual_focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "actual_focus_value: "+ actual_focus_value); + String compare_focus_value = focus_value; + if( compare_focus_value.equals("focus_mode_locked") ) + compare_focus_value = "focus_mode_auto"; + else if( compare_focus_value.equals("focus_mode_infinity") && mPreview.usingCamera2API() ) + compare_focus_value = "focus_mode_manual2"; + assertTrue(compare_focus_value.equals(actual_focus_value)); + } + } + + private void switchToISO(int required_iso) { + Log.d(TAG, "switchToISO: "+ required_iso); + if( mPreview.supportsFocus() ) { + int iso = mPreview.getCameraController().getISO(); + Log.d(TAG, "start iso: "+ iso); + if( iso != required_iso ) { + assertFalse( mActivity.popupIsOpen() ); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + View isoButton = mActivity.getPopupButton("TEST_ISO_" + required_iso); + assertTrue(isoButton != null); + clickView(isoButton); + iso = mPreview.getCameraController().getISO(); + Log.d(TAG, "changed iso to: "+ iso); + } + assertTrue(iso == required_iso); + } + } + + /* Sets the camera up to a predictable state: + * - Back camera + * - Photo mode + * - Flash off (if flash supported) + * - Focus mode auto (if focus modes supported) + * As a side-effect, the camera and/or camera parameters values may become invalid. + */ + private void setToDefault() { + if( mPreview.isVideo() ) { + Log.d(TAG, "turn off video mode"); + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + } + assertTrue(!mPreview.isVideo()); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + int cameraId = mPreview.getCameraId(); + Log.d(TAG, "start cameraId: "+ cameraId); + while( cameraId != 0 ) { + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + // camera becomes invalid when switching cameras + cameraId = mPreview.getCameraId(); + Log.d(TAG, "changed cameraId to: "+ cameraId); + } + } + + switchToFlashValue("flash_off"); + switchToFocusValue("focus_mode_auto"); + } + + /* Ensures that we only start the camera preview once when starting up. + */ + public void testStartCameraPreviewCount() { + Log.d(TAG, "testStartCameraPreviewCount"); + /*Log.d(TAG, "1 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + int init_count_cameraStartPreview = mPreview.count_cameraStartPreview; + mActivity.finish(); + setActivity(null); + mActivity = this.getActivity(); + mPreview = mActivity.getPreview(); + Log.d(TAG, "2 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == init_count_cameraStartPreview); + this.getInstrumentation().callActivityOnPause(mActivity); + Log.d(TAG, "3 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == init_count_cameraStartPreview); + this.getInstrumentation().callActivityOnResume(mActivity); + Log.d(TAG, "4 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == init_count_cameraStartPreview+1);*/ + setToDefault(); + + restart(); + // onResume has code that must run on UI thread + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "1 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == 1); + getInstrumentation().callActivityOnPause(mActivity); + Log.d(TAG, "2 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == 1); + getInstrumentation().callActivityOnResume(mActivity); + Log.d(TAG, "3 count_cameraStartPreview: " + mPreview.count_cameraStartPreview); + assertTrue(mPreview.count_cameraStartPreview == 2); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + } + + /* Ensures that we save the video mode. + * Also tests the icons and content descriptions of the take photo and switch photo/video buttons are as expected. + */ + public void testSaveVideoMode() { + Log.d(TAG, "testSaveVideoMode"); + setToDefault(); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + + assertTrue(!mPreview.isVideo()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.take_photo) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_video) ) ); + + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_photo) ) ); + + restart(); + assertTrue(mPreview.isVideo()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_photo) ) ); + + pauseAndResume(); + assertTrue(mPreview.isVideo()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_photo) ) ); + } + + /* Ensures that we save the focus mode for photos when restarting. + * Note that saving the focus mode for video mode is tested in testFocusSwitchVideoResetContinuous. + */ + public void testSaveFocusMode() { + Log.d(TAG, "testSaveVideoMode"); + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_macro"); + + restart(); + String focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_macro")); + + pauseAndResume(); + focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_macro")); + } + + /* Ensures that we save the flash mode torch when quitting and restarting. + */ + public void testSaveFlashTorchQuit() throws InterruptedException { + Log.d(TAG, "testSaveFlashTorchQuit"); + + if( !mPreview.supportsFlash() ) { + return; + } + + setToDefault(); + + switchToFlashValue("flash_torch"); + + restart(); + Thread.sleep(4000); // needs to be long enough for the autofocus to complete + String controller_flash_value = mPreview.getCameraController().getFlashValue(); + Log.d(TAG, "controller_flash_value: " + controller_flash_value); + assertTrue(controller_flash_value.equals("flash_torch")); + String flash_value = mPreview.getCurrentFlashValue(); + Log.d(TAG, "flash_value: " + flash_value); + assertTrue(flash_value.equals("flash_torch")); + } + + /* Ensures that we save the flash mode torch when switching to front camera and then to back + * Note that this sometimes fail on Galaxy Nexus, because flash turns off after autofocus (and other camera apps do this too), but this only seems to happen some of the time! + * And Nexus 7 has no flash anyway. + * So commented out test for now. + */ + /*public void testSaveFlashTorchSwitchCamera() { + Log.d(TAG, "testSaveFlashTorchSwitchCamera"); + + if( !mPreview.supportsFlash() ) { + return; + } + else if( Camera.getNumberOfCameras() <= 1 ) { + return; + } + + setToDefault(); + + switchToFlashValue("flash_torch"); + + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + + Camera camera = mPreview.getCamera(); + Camera.Parameters parameters = camera.getParameters(); + Log.d(TAG, "parameters flash mode: " + parameters.getFlashMode()); + assertTrue(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)); + String flash_value = mPreview.getCurrentFlashValue(); + Log.d(TAG, "flash_value: " + flash_value); + assertTrue(flash_value.equals("flash_torch")); + }*/ + + public void testFlashStartup() throws InterruptedException { + Log.d(TAG, "testFlashStartup"); + setToDefault(); + + if( !mPreview.supportsFlash() ) { + return; + } + + Log.d(TAG, "# switch to flash on"); + switchToFlashValue("flash_on"); + Log.d(TAG, "# restart"); + restart(); + + Log.d(TAG, "# switch flash mode"); + // now switch to torch - the idea is that this is done while the camera is starting up + // though note that sometimes we might not be quick enough here! + // don't use switchToFlashValue here, it'll get confused due to the autofocus changing the parameters flash mode + // update: now okay to use it, now we have the popup UI + //View flashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.flash); + //clickView(flashButton); + switchToFlashValue("flash_torch"); + + //Camera camera = mPreview.getCamera(); + //Camera.Parameters parameters = camera.getParameters(); + //String flash_mode = mPreview.getCurrentFlashMode(); + String flash_value = mPreview.getCurrentFlashValue(); + Log.d(TAG, "# flash value is now: " + flash_value); + Log.d(TAG, "# sleep"); + Thread.sleep(4000); // needs to be long enough for the autofocus to complete + /*parameters = camera.getParameters(); + Log.d(TAG, "# parameters flash mode: " + parameters.getFlashMode()); + assertTrue(parameters.getFlashMode().equals(flash_mode));*/ + String camera_flash_value = mPreview.getCameraController().getFlashValue(); + Log.d(TAG, "# camera flash value: " + camera_flash_value); + assertTrue(camera_flash_value.equals(flash_value)); + } + + /** Tests that flash remains on, with the startup focus flash hack. + */ + public void testFlashStartup2() throws InterruptedException { + Log.d(TAG, "testFlashStartup2"); + setToDefault(); + + if( !mPreview.supportsFlash() ) { + return; + } + + Log.d(TAG, "# switch to flash on"); + switchToFlashValue("flash_on"); + Log.d(TAG, "# restart"); + restart(); + Thread.sleep(3000); + String flash_value = mPreview.getCameraController().getFlashValue(); + Log.d(TAG, "1 flash value is now: " + flash_value); + assertTrue(flash_value.equals("flash_on")); + + switchToFocusValue("focus_mode_continuous_picture"); + restart(); + Thread.sleep(3000); + flash_value = mPreview.getCameraController().getFlashValue(); + Log.d(TAG, "2 flash value is now: " + flash_value); + assertTrue(flash_value.equals("flash_on")); + } + + private void checkOptimalPreviewSize() { + Log.d(TAG, "preview size: " + mPreview.getCameraController().getPreviewSize().width + ", " + mPreview.getCameraController().getPreviewSize().height); + List sizes = mPreview.getSupportedPreviewSizes(); + CameraController.Size best_size = mPreview.getOptimalPreviewSize(sizes); + Log.d(TAG, "best size: " + best_size.width + ", " + best_size.height); + assertTrue( best_size.width == mPreview.getCameraController().getPreviewSize().width ); + assertTrue( best_size.height == mPreview.getCameraController().getPreviewSize().height ); + } + + private void checkOptimalVideoPictureSize(double targetRatio) { + // even the picture resolution should have same aspect ratio for video - otherwise have problems on Nexus 7 with Android 4.4.3 + Log.d(TAG, "video picture size: " + mPreview.getCameraController().getPictureSize().width + ", " + mPreview.getCameraController().getPictureSize().height); + List sizes = mPreview.getSupportedPictureSizes(); + CameraController.Size best_size = mPreview.getOptimalVideoPictureSize(sizes, targetRatio); + Log.d(TAG, "best size: " + best_size.width + ", " + best_size.height); + assertTrue( best_size.width == mPreview.getCameraController().getPictureSize().width ); + assertTrue( best_size.height == mPreview.getCameraController().getPictureSize().height ); + } + + private void checkSquareAspectRatio() { + Log.d(TAG, "preview size: " + mPreview.getCameraController().getPreviewSize().width + ", " + mPreview.getCameraController().getPreviewSize().height); + Log.d(TAG, "frame size: " + mPreview.getView().getWidth() + ", " + mPreview.getView().getHeight()); + double frame_aspect_ratio = ((double)mPreview.getView().getWidth()) / (double)mPreview.getView().getHeight(); + double preview_aspect_ratio = ((double)mPreview.getCameraController().getPreviewSize().width) / (double)mPreview.getCameraController().getPreviewSize().height; + Log.d(TAG, "frame_aspect_ratio: " + frame_aspect_ratio); + Log.d(TAG, "preview_aspect_ratio: " + preview_aspect_ratio); + // we calculate etol like this, due to errors from rounding + //double etol = 1.0f / Math.min((double)mPreview.getWidth(), (double)mPreview.getHeight()) + 1.0e-5; + double etol = (double)mPreview.getView().getWidth() / (double)(mPreview.getView().getHeight() * (mPreview.getView().getHeight()-1) ) + 1.0e-5; + assertTrue( Math.abs(frame_aspect_ratio - preview_aspect_ratio) <= etol ); + } + + /* Ensures that preview resolution is set as expected in non-WYSIWYG mode + */ + public void testPreviewSize() { + Log.d(TAG, "testPreviewSize"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPreviewSizePreferenceKey(), "preference_preview_size_display"); + editor.apply(); + updateForSettings(); + + Point display_size = new Point(); + { + Display display = mActivity.getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y); + } + //double targetRatio = mPreview.getTargetRatioForPreview(display_size); + double targetRatio = mPreview.getTargetRatio(); + double expTargetRatio = ((double)display_size.x) / (double)display_size.y; + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + + //targetRatio = mPreview.getTargetRatioForPreview(display_size); + targetRatio = mPreview.getTargetRatio(); + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + } + } + + /* Ensures that preview resolution is set as expected in WYSIWYG mode + */ + public void testPreviewSizeWYSIWYG() { + Log.d(TAG, "testPreviewSizeWYSIWYG"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPreviewSizePreferenceKey(), "preference_preview_size_wysiwyg"); + editor.apply(); + updateForSettings(); + + Point display_size = new Point(); + { + Display display = mActivity.getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y); + } + CameraController.Size picture_size = mPreview.getCameraController().getPictureSize(); + CameraController.Size preview_size = mPreview.getCameraController().getPreviewSize(); + //double targetRatio = mPreview.getTargetRatioForPreview(display_size); + double targetRatio = mPreview.getTargetRatio(); + double expTargetRatio = ((double)picture_size.width) / (double)picture_size.height; + double previewRatio = ((double)preview_size.width) / (double)preview_size.height; + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + assertTrue( Math.abs(previewRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + + Log.d(TAG, "switch to video"); + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + CamcorderProfile profile = mPreview.getCamcorderProfile(); + CameraController.Size video_preview_size = mPreview.getCameraController().getPreviewSize(); + //targetRatio = mPreview.getTargetRatioForPreview(display_size); + targetRatio = mPreview.getTargetRatio(); + expTargetRatio = ((double)profile.videoFrameWidth) / (double)profile.videoFrameHeight; + previewRatio = ((double)video_preview_size.width) / (double)video_preview_size.height; + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + assertTrue( Math.abs(previewRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + checkOptimalVideoPictureSize(expTargetRatio); + + clickView(switchVideoButton); + assertTrue(!mPreview.isVideo()); + CameraController.Size new_picture_size = mPreview.getCameraController().getPictureSize(); + CameraController.Size new_preview_size = mPreview.getCameraController().getPreviewSize(); + Log.d(TAG, "picture_size: " + picture_size.width + " x " + picture_size.height); + Log.d(TAG, "new_picture_size: " + new_picture_size.width + " x " + new_picture_size.height); + Log.d(TAG, "preview_size: " + preview_size.width + " x " + preview_size.height); + Log.d(TAG, "new_preview_size: " + new_preview_size.width + " x " + new_preview_size.height); + assertTrue(new_picture_size.equals(picture_size)); + assertTrue(new_preview_size.equals(preview_size)); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + + picture_size = mPreview.getCameraController().getPictureSize(); + preview_size = mPreview.getCameraController().getPreviewSize(); + //targetRatio = mPreview.getTargetRatioForPreview(display_size); + targetRatio = mPreview.getTargetRatio(); + expTargetRatio = ((double)picture_size.width) / (double)picture_size.height; + previewRatio = ((double)preview_size.width) / (double)preview_size.height; + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + assertTrue( Math.abs(previewRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + + Log.d(TAG, "switch to video again"); + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + profile = mPreview.getCamcorderProfile(); + video_preview_size = mPreview.getCameraController().getPreviewSize(); + //targetRatio = mPreview.getTargetRatioForPreview(display_size); + targetRatio = mPreview.getTargetRatio(); + expTargetRatio = ((double)profile.videoFrameWidth) / (double)profile.videoFrameHeight; + previewRatio = ((double)video_preview_size.width) / (double)video_preview_size.height; + assertTrue( Math.abs(targetRatio - expTargetRatio) <= 1.0e-5 ); + assertTrue( Math.abs(previewRatio - expTargetRatio) <= 1.0e-5 ); + checkOptimalPreviewSize(); + checkSquareAspectRatio(); + checkOptimalVideoPictureSize(expTargetRatio); + + clickView(switchVideoButton); + assertTrue(!mPreview.isVideo()); + new_picture_size = mPreview.getCameraController().getPictureSize(); + new_preview_size = mPreview.getCameraController().getPreviewSize(); + assertTrue(new_picture_size.equals(picture_size)); + assertTrue(new_preview_size.equals(preview_size)); + } + } + + /* Tests camera error handling. + */ + public void testOnError() { + Log.d(TAG, "testOnError"); + setToDefault(); + + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "onError..."); + mPreview.getCameraController().onError(); + } + }); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCameraController() == null ); + } + + /* Various tests for auto-focus. + */ + public void testAutoFocus() throws InterruptedException { + Log.d(TAG, "testAutoFocus"); + if( !mPreview.supportsFocus() ) { + return; + } + //int saved_count = mPreview.count_cameraAutoFocus; + int saved_count = 0; // set to 0 rather than count_cameraAutoFocus, as on Galaxy Nexus, it can happen that startup autofocus has already occurred by the time we reach here + Log.d(TAG, "saved_count: " + saved_count); + setToDefault(); + switchToFocusValue("focus_mode_auto"); + + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + Thread.sleep(1000); // wait until autofocus startup + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // touch to auto-focus with focus area + saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "2 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + + saved_count = mPreview.count_cameraAutoFocus; + // test selecting same mode doesn't set off an autofocus or reset the focus area + switchToFocusValue("focus_mode_auto"); + Log.d(TAG, "3 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + + saved_count = mPreview.count_cameraAutoFocus; + // test switching mode sets off an autofocus, and resets the focus area + switchToFocusValue("focus_mode_macro"); + Log.d(TAG, "4 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + saved_count = mPreview.count_cameraAutoFocus; + // switching to focus locked shouldn't set off an autofocus + switchToFocusValue("focus_mode_locked"); + Log.d(TAG, "5 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + + saved_count = mPreview.count_cameraAutoFocus; + // touch to focus should autofocus + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "6 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + + saved_count = mPreview.count_cameraAutoFocus; + // switching to focus continuous shouldn't set off an autofocus + switchToFocusValue("focus_mode_continuous_picture"); + Log.d(TAG, "7 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(!mPreview.isFocusWaiting()); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + + // but touch to focus should + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "8 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + + switchToFocusValue("focus_mode_locked"); // change to a mode that isn't auto (so that the first iteration of the next loop will set of an autofocus, due to changing the focus mode) + List supported_focus_values = mPreview.getSupportedFocusValues(); + assertTrue( supported_focus_values != null ); + assertTrue( supported_focus_values.size() > 1 ); + for(String supported_focus_value : supported_focus_values) { + Log.d(TAG, "supported_focus_value: " + supported_focus_value); + saved_count = mPreview.count_cameraAutoFocus; + Log.d(TAG, "saved autofocus count: " + saved_count); + //View focusModeButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + //clickView(focusModeButton); + switchToFocusValue(supported_focus_value); + // test that switching focus mode resets the focus area + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + // test that switching focus mode sets off an autofocus in focus auto or macro mode + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "changed focus_value to: "+ focus_value); + Log.d(TAG, "count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") ) { + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + } + else { + assertTrue(!mPreview.isFocusWaiting()); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + + // test that touch to auto-focus region only works in focus auto, macro or continuous mode, and that we set off an autofocus for focus auto and macro + // test that touch to set metering area works in any focus mode + saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") || focus_value.equals("focus_mode_continuous_picture") || focus_value.equals("focus_mode_continuous_video") ) { + if( focus_value.equals("focus_mode_continuous_picture") || focus_value.equals("focus_mode_continuous_video") ) { + assertTrue(!mPreview.isFocusWaiting()); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + else { + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + } + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + else { + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + // also check that focus mode is unchanged + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + if( focus_value.equals("focus_mode_auto") ) { + break; + } + } + } + + /* Test we do startup autofocus as expected depending on focus mode. + */ + public void testStartupAutoFocus() throws InterruptedException { + Log.d(TAG, "testStartupAutoFocus"); + if( !mPreview.supportsFocus() ) { + return; + } + //int saved_count = mPreview.count_cameraAutoFocus; + int saved_count = 0; // set to 0 rather than count_cameraAutoFocus, as on Galaxy Nexus, it can happen that startup autofocus has already occurred by the time we reach here + Log.d(TAG, "saved_count: " + saved_count); + setToDefault(); + switchToFocusValue("focus_mode_auto"); + + Thread.sleep(1000); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + + restart(); + //saved_count = mPreview.count_cameraAutoFocus; + saved_count = 0; + Log.d(TAG, "saved_count: " + saved_count); + Thread.sleep(1000); + Log.d(TAG, "2 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + + if( mPreview.getSupportedFocusValues().contains("focus_mode_infinity") ) { + switchToFocusValue("focus_mode_infinity"); + restart(); + //saved_count = mPreview.count_cameraAutoFocus; + saved_count = 0; + Log.d(TAG, "saved_count: " + saved_count); + Thread.sleep(1000); + Log.d(TAG, "3 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + + if( mPreview.getSupportedFocusValues().contains("focus_mode_macro") ) { + switchToFocusValue("focus_mode_macro"); + restart(); + //saved_count = mPreview.count_cameraAutoFocus; + saved_count = 0; + Log.d(TAG, "saved_count: " + saved_count); + Thread.sleep(1000); + Log.d(TAG, "4 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + } + + if( mPreview.getSupportedFocusValues().contains("focus_mode_locked") ) { + switchToFocusValue("focus_mode_locked"); + restart(); + //saved_count = mPreview.count_cameraAutoFocus; + saved_count = 0; + Log.d(TAG, "saved_count: " + saved_count); + Thread.sleep(1000); + Log.d(TAG, "5 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + } + + if( mPreview.getSupportedFocusValues().contains("focus_mode_continuous_picture") ) { + switchToFocusValue("focus_mode_continuous_picture"); + restart(); + //saved_count = mPreview.count_cameraAutoFocus; + saved_count = 0; + Log.d(TAG, "saved_count: " + saved_count); + Thread.sleep(1000); + Log.d(TAG, "6 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus + " compare to saved_count: " + saved_count); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + } + + /* Test doing touch to auto-focus region by swiping to all four corners works okay. + */ + public void testAutoFocusCorners() { + Log.d(TAG, "testAutoFocusCorners"); + if( !mPreview.supportsFocus() ) { + return; + } + setToDefault(); + int [] gui_location = new int[2]; + mPreview.getView().getLocationOnScreen(gui_location); + final int step_dist_c = 2; + final float scale = mActivity.getResources().getDisplayMetrics().density; + final int large_step_dist_c = (int) (60 * scale + 0.5f); // convert dps to pixels + final int step_count_c = 10; + int width = mPreview.getView().getWidth(); + int height = mPreview.getView().getHeight(); + Log.d(TAG, "preview size: " + width + " x " + height); + + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + Log.d(TAG, "top-left"); + TouchUtils.drag(MainActivityTest.this, gui_location[0] + step_dist_c, gui_location[0], gui_location[1] + step_dist_c, gui_location[1], step_count_c); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + + mPreview.clearFocusAreas(); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // do larger step at top-right, due to conflicting with Settings button + // but we now ignore swipes - so we now test for that instead + Log.d(TAG, "top-right"); + TouchUtils.drag(MainActivityTest.this, gui_location[0]+width-1-large_step_dist_c, gui_location[0]+width-1, gui_location[1]+large_step_dist_c, gui_location[1], step_count_c); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + Log.d(TAG, "bottom-left"); + TouchUtils.drag(MainActivityTest.this, gui_location[0]+step_dist_c, gui_location[0], gui_location[1]+height-1-step_dist_c, gui_location[1]+height-1, step_count_c); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + + mPreview.clearFocusAreas(); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // skip bottom right, conflicts with zoom on various devices + } + + /* Test face detection, and that we don't get the focus/metering areas set. + */ + public void testFaceDetection() throws InterruptedException { + Log.d(TAG, "testFaceDetection"); + if( !mPreview.supportsFaceDetection() ) { + Log.d(TAG, "face detection not supported"); + return; + } + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getFaceDetectionPreferenceKey(), true); + editor.apply(); + updateForSettings(); + + int saved_count = mPreview.count_cameraAutoFocus; + Log.d(TAG, "0 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + // autofocus shouldn't be immediately, but after a delay + Thread.sleep(1000); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + boolean face_detection_started = false; + if( !mPreview.getCameraController().startFaceDetection() ) { + // should throw RuntimeException if face detection already started + face_detection_started = true; + } + assertTrue(face_detection_started); + + // touch to auto-focus with focus area + saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "2 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + face_detection_started = false; + if( !mPreview.getCameraController().startFaceDetection() ) { + // should throw RuntimeException if face detection already started + face_detection_started = true; + } + assertTrue(face_detection_started); + } + } + + private void subTestPopupButtonAvailability(String test_key, String option, boolean expected) { + View button = mActivity.getPopupButton(test_key + "_" + option); + if( expected ) { + boolean is_video = mPreview.isVideo(); + if( option.equals("focus_mode_continuous_picture") && is_video ) { + // not allowed in video mode + assertTrue(button == null); + } + else if( option.equals("focus_mode_continuous_video") && !is_video ) { + // not allowed in picture mode + assertTrue(button == null); + } + else { + assertTrue(button != null); + } + } + else { + Log.d(TAG, "option? "+ option); + Log.d(TAG, "button? "+ button); + assertTrue(button == null); + } + } + + private void subTestPopupButtonAvailability(String test_key, String option, List options) { + subTestPopupButtonAvailability(test_key, option, options != null && options.contains(option)); + } + + private void subTestPopupButtonAvailability(String option, boolean expected) { + View button = mActivity.getPopupButton(option); + if( expected ) { + assertTrue(button != null); + } + else { + assertTrue(button == null); + } + } + + private void subTestPopupButtonAvailability() { + List supported_flash_values = mPreview.getSupportedFlashValues(); + subTestPopupButtonAvailability("TEST_FLASH", "flash_off", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_auto", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_on", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_torch", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_red_eye", supported_flash_values); + List supported_focus_values = mPreview.getSupportedFocusValues(); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_auto", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_locked", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_infinity", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_macro", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_fixed", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_edof", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_continuous_picture", supported_focus_values); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_continuous_video", supported_focus_values); + if( mPreview.supportsISORange() ) { + subTestPopupButtonAvailability("TEST_ISO", "auto", true); + subTestPopupButtonAvailability("TEST_ISO", "100", true); + subTestPopupButtonAvailability("TEST_ISO", "200", true); + subTestPopupButtonAvailability("TEST_ISO", "400", true); + subTestPopupButtonAvailability("TEST_ISO", "800", true); + subTestPopupButtonAvailability("TEST_ISO", "1600", true); + } + else { + List supported_iso_values = mPreview.getSupportedISOs(); + subTestPopupButtonAvailability("TEST_ISO", "auto", supported_iso_values); + subTestPopupButtonAvailability("TEST_ISO", "100", supported_iso_values); + subTestPopupButtonAvailability("TEST_ISO", "200", supported_iso_values); + subTestPopupButtonAvailability("TEST_ISO", "400", supported_iso_values); + subTestPopupButtonAvailability("TEST_ISO", "800", supported_iso_values); + subTestPopupButtonAvailability("TEST_ISO", "1600", supported_iso_values); + } + subTestPopupButtonAvailability("TEST_WHITE_BALANCE", mPreview.getSupportedWhiteBalances() != null); + subTestPopupButtonAvailability("TEST_SCENE_MODE", mPreview.getSupportedSceneModes() != null); + subTestPopupButtonAvailability("TEST_COLOR_EFFECT", mPreview.getSupportedColorEffects() != null); + } + + private void subTestFocusFlashAvailability() { + //View focusModeButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + //View flashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.flash); + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + /*boolean focus_visible = focusModeButton.getVisibility() == View.VISIBLE; + Log.d(TAG, "focus_visible? "+ focus_visible); + boolean flash_visible = flashButton.getVisibility() == View.VISIBLE; + Log.d(TAG, "flash_visible? "+ flash_visible);*/ + boolean exposure_visible = exposureButton.getVisibility() == View.VISIBLE; + Log.d(TAG, "exposure_visible? "+ exposure_visible); + boolean exposure_lock_visible = exposureLockButton.getVisibility() == View.VISIBLE; + Log.d(TAG, "exposure_lock_visible? "+ exposure_lock_visible); + boolean popup_visible = popupButton.getVisibility() == View.VISIBLE; + Log.d(TAG, "popup_visible? "+ popup_visible); + boolean has_focus = mPreview.supportsFocus(); + Log.d(TAG, "has_focus? "+ has_focus); + boolean has_flash = mPreview.supportsFlash(); + Log.d(TAG, "has_flash? "+ has_flash); + boolean has_exposure = mPreview.supportsExposures(); + Log.d(TAG, "has_exposure? "+ has_exposure); + boolean has_exposure_lock = mPreview.supportsExposureLock(); + Log.d(TAG, "has_exposure_lock? "+ has_exposure_lock); + //assertTrue(has_focus == focus_visible); + //assertTrue(has_flash == flash_visible); + assertTrue(has_exposure == exposure_visible); + assertTrue(has_exposure_lock == exposure_lock_visible); + assertTrue(popup_visible); + + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + subTestPopupButtonAvailability(); + } + + /* + * For each camera, test that visibility of flash and focus etc buttons matches the availability of those camera parameters. + * Added to guard against a bug where on Nexus 7, the flash and focus buttons were made visible by showGUI, even though they aren't supported by Nexus 7 front camera. + */ + public void testFocusFlashAvailability() { + Log.d(TAG, "testFocusFlashAvailability"); + setToDefault(); + + subTestFocusFlashAvailability(); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + int cameraId = mPreview.getCameraId(); + Log.d(TAG, "cameraId? "+ cameraId); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + //mActivity.clickedSwitchCamera(switchCameraButton); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + Log.d(TAG, "new_cameraId? "+ new_cameraId); + assertTrue(cameraId != new_cameraId); + + subTestFocusFlashAvailability(); + } + } + + /* Tests switching to/from video mode, for front and back cameras, and tests the focus mode changes as expected. + */ + public void testSwitchVideo() throws InterruptedException { + Log.d(TAG, "testSwitchVideo"); + + setToDefault(); + assertTrue(!mPreview.isVideo()); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + if( mPreview.supportsFocus() ) { + assertTrue(focus_value.equals("focus_mode_continuous_video")); + } + + int saved_count = mPreview.count_cameraAutoFocus; + Log.d(TAG, "0 count_cameraAutoFocus: " + saved_count); + clickView(switchVideoButton); + assertTrue(!mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "picture focus_value: "+ focus_value); + if( mPreview.supportsFocus() ) { + assertTrue(focus_value.equals("focus_mode_auto")); + // check that this doesn't cause an autofocus + assertTrue(!mPreview.isFocusWaiting()); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "front picture focus_value: "+ focus_value); + if( mPreview.supportsFocus() ) { + assertTrue(focus_value.equals("focus_mode_auto")); + } + + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "front video focus_value: "+ focus_value); + if( mPreview.supportsFocus() ) { + assertTrue(focus_value.equals("focus_mode_continuous_video")); + } + + clickView(switchVideoButton); + assertTrue(!mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "front picture focus_value: "+ focus_value); + if( mPreview.supportsFocus() ) { + assertTrue(focus_value.equals("focus_mode_auto")); + } + + // now switch back + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + } + + if( mPreview.supportsFocus() ) { + // now test we remember the focus mode for photo and video + + switchToFocusValue("focus_mode_continuous_picture"); + + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + switchToFocusValue("focus_mode_macro"); + + clickView(switchVideoButton); + assertTrue(!mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "picture focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_picture")); + + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_macro")); + } + + + + } + + /* Tests continuous picture focus, including switching to video and back. + * Tends to fail on Galaxy Nexus, where the continuous picture focusing doesn't happen too often. + */ + public void testContinuousPictureFocus() throws InterruptedException { + Log.d(TAG, "testContinuousPictureFocus"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + // first switch to auto-focus (if we're already in continuous picture mode, we might have already done the continuous focus moving + switchToFocusValue("focus_mode_auto"); + pauseAndResume(); + switchToFocusValue("focus_mode_continuous_picture"); + + // check continuous focus is working + int saved_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Thread.sleep(1000); + int new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Log.d(TAG, "count_cameraContinuousFocusMoving compare saved: "+ saved_count_cameraContinuousFocusMoving + " to new: " + new_count_cameraContinuousFocusMoving); + assertTrue( new_count_cameraContinuousFocusMoving > saved_count_cameraContinuousFocusMoving ); + + // switch to video + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + // switch to photo + clickView(switchVideoButton); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_picture")); + + // check continuous focus is working + saved_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Thread.sleep(3000); + new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Log.d(TAG, "count_cameraContinuousFocusMoving compare saved: "+ saved_count_cameraContinuousFocusMoving + " to new: " + new_count_cameraContinuousFocusMoving); + assertTrue( new_count_cameraContinuousFocusMoving > saved_count_cameraContinuousFocusMoving ); + } + + /* Tests everything works okay if starting in continuous video focus mode when in photo mode, including opening popup, and switching to video and back. + * This shouldn't be possible normal, but could happen if a user is upgrading from version 1.28 or earlier, to version 1.29 or later. + */ + public void testContinuousVideoFocusForPhoto() throws InterruptedException { + Log.d(TAG, "testContinuousVideoFocusForPhoto"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getFocusPreferenceKey(mPreview.getCameraId(), false), "focus_mode_continuous_video"); + editor.apply(); + restart(); + + Thread.sleep(1000); + + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + + Thread.sleep(1000); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + } + + /* Tests continuous picture focus with burst mode. + */ + public void testContinuousPictureFocusBurst() throws InterruptedException { + Log.d(TAG, "testContinuousPictureFocusBurst"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getBurstModePreferenceKey(), "3"); + editor.apply(); + } + switchToFocusValue("focus_mode_continuous_picture"); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + assertTrue(!mPreview.isOnTimer()); + + // wait until photos taken + // wait 15s, and test that we've taken the photos by then + while( mPreview.count_cameraTakePicture < 3 ) { + } + Thread.sleep(1500); // allow pictures to save + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + Log.d(TAG, "count_cameraTakePicture: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==3); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 3); + } + + /* Test for continuous picture photo mode. + * Touch, wait 8s, check that continuous focus mode has resumed, then take photo. + */ + public void testContinuousPicture1() throws InterruptedException { + Log.d(TAG, "testContinuousPicture1"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_continuous_picture"); + + String focus_value = "focus_mode_continuous_picture"; + String focus_value_ui = "focus_mode_continuous_picture"; + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + Thread.sleep(1000); + assertTrue(mPreview.count_cameraTakePicture==0); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + Log.d(TAG, "about to click preview for autofocus"); + int saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_continuous_picture")); + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + Thread.sleep(8000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + Log.d(TAG, "done taking photo"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + assertTrue(mPreview.count_cameraTakePicture==1); + mActivity.waitUntilImageQueueEmpty(); + + assertTrue( folder.exists() ); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + } + + /* Test for continuous picture photo mode. + * Touch, wait 1s, check that continuous focus mode hasn't resumed, then take photo, then check continuous focus mode has resumed. + */ + public void testContinuousPicture2() throws InterruptedException { + Log.d(TAG, "testContinuousPicture1"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_continuous_picture"); + + String focus_value = "focus_mode_continuous_picture"; + String focus_value_ui = "focus_mode_continuous_picture"; + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + Thread.sleep(1000); + assertTrue(mPreview.count_cameraTakePicture==0); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + Log.d(TAG, "about to click preview for autofocus"); + int saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_continuous_picture")); + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + int saved_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + + Thread.sleep(1000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + int new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + Log.d(TAG, "2 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + Log.d(TAG, "done taking photo"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + assertTrue(mPreview.count_cameraTakePicture==1); + Log.d(TAG, "3 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + mActivity.waitUntilImageQueueEmpty(); + + assertTrue( folder.exists() ); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + } + + /* Test for continuous picture photo mode. + * Touch repeatedly with 1s delays for 8 times, make sure continuous focus mode hasn't resumed. + * Then wait 5s, and check continuous focus mode has resumed. + */ + public void testContinuousPictureRepeatTouch() throws InterruptedException { + Log.d(TAG, "testContinuousPictureRepeatTouch"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_continuous_picture"); + + String focus_value = "focus_mode_continuous_picture"; + String focus_value_ui = "focus_mode_continuous_picture"; + + Thread.sleep(1000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + for(int i=0;i<8;i++) { + Log.d(TAG, "about to click preview for autofocus: " + i); + int saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + int saved_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Thread.sleep(1000); + + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_continuous_picture")); + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + int new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + } + + int saved_count = mPreview.count_cameraAutoFocus; + Thread.sleep(5000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + Log.d(TAG, "2 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count); + } + + /* Test for continuous picture photo mode. + * Touch, then after 1s switch to focus auto in UI, wait 8s, ensure still in autofocus mode. + */ + public void testContinuousPictureSwitchAuto() throws InterruptedException { + Log.d(TAG, "testContinuousPictureSwitchAuto"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_continuous_picture"); + + String focus_value = "focus_mode_continuous_picture"; + String focus_value_ui = "focus_mode_continuous_picture"; + + Thread.sleep(1000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + Log.d(TAG, "about to click preview for autofocus"); + int saved_count = mPreview.count_cameraAutoFocus; + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == saved_count+1); + int saved_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + Thread.sleep(1000); + + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + int new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + + Thread.sleep(1000); + assertTrue(mPreview.getCurrentFocusValue().equals(focus_value_ui)); + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + + switchToFocusValue("focus_mode_auto"); + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_auto")); + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); + new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + + Thread.sleep(8000); + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_auto")); + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); + new_count_cameraContinuousFocusMoving = mPreview.count_cameraContinuousFocusMoving; + assertTrue( new_count_cameraContinuousFocusMoving == saved_count_cameraContinuousFocusMoving ); + } + + /* Start in photo mode with auto focus: + * - go to video mode + * - then switch to front camera + * - then stop video + * - then go to back camera + * Check focus mode has returned to auto. + * This test is important when front camera doesn't support focus modes, but back camera does - we won't be able to reset to auto focus for the front camera, but need to do so when returning to back camera + */ + public void testFocusSwitchVideoSwitchCameras() { + Log.d(TAG, "testFocusSwitchVideoSwitchCameras"); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "video focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + // camera becomes invalid when switching cameras + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "front video focus_value: "+ focus_value); + // don't care when focus mode is for front camera (focus may not be supported for front camera) + + clickView(switchVideoButton); + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "front focus_value: "+ focus_value); + // don't care when focus mode is for front camera (focus may not be supported for front camera) + + clickView(switchCameraButton); + // camera becomes invalid when switching cameras + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "end focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_auto")); + } + + /* Start in photo mode with focus macro: + * - switch to front camera + * - switch to back camera + * Check focus mode is still macro. + * This test is important when front camera doesn't support focus modes, but back camera does - need to remain in macro mode for the back camera. + */ + public void testFocusRemainMacroSwitchCamera() { + Log.d(TAG, "testFocusRemainMacroSwitchCamera"); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_macro"); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + // n.b., call twice, to switch to front then to back + clickView(switchCameraButton); + clickView(switchCameraButton); + + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "focus_value: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_macro")); + } + + /* Start in photo mode with focus auto: + * - switch to video mode + * - switch to focus macro + * - switch to picture mode + * Check focus mode is now auto. + * As of 1.26, we now remember the focus mode for photos. + */ + public void testFocusRemainMacroSwitchPhoto() { + Log.d(TAG, "testFocusRemainMacroSwitchPhoto"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "focus_value after switching to video mode: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + switchToFocusValue("focus_mode_macro"); + + clickView(switchVideoButton); + + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "focus_value after switching to picture mode: " + focus_value); + assertTrue(focus_value.equals("focus_mode_auto")); + } + + /* Start in photo mode with focus auto: + * - switch to focus macro + * - switch to video mode + * - switch to picture mode + * Check focus mode is still macro. + * As of 1.26, we now remember the focus mode for photos. + */ + public void testFocusSaveMacroSwitchPhoto() { + Log.d(TAG, "testFocusSaveMacroSwitchPhoto"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + + switchToFocusValue("focus_mode_macro"); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + String focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "focus_value after switching to video mode: "+ focus_value); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + clickView(switchVideoButton); + + focus_value = mPreview.getCameraController().getFocusValue(); + Log.d(TAG, "focus_value after switching to picture mode: " + focus_value); + assertTrue(focus_value.equals("focus_mode_macro")); + } + + /* Start in photo mode with auto focus: + * - go to video mode + * - check in continuous focus mode + * - switch to auto focus mode + * - then pause and resume + * - then check still in video mode, still in auto focus mode + * - then repeat with restarting instead + * (Note the name is a bit misleading - it used to be that we reset to continuous mode, now we don't.) + */ + public void testFocusSwitchVideoResetContinuous() { + Log.d(TAG, "testFocusSwitchVideoResetContinuous"); + + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + switchToFocusValue("focus_mode_auto"); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + String focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_continuous_video")); + + switchToFocusValue("focus_mode_auto"); + focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_auto")); + + this.pauseAndResume(); + assertTrue(mPreview.isVideo()); + + focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_auto")); + + // now with restart + + switchToFocusValue("focus_mode_auto"); + focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_auto")); + + restart(); + assertTrue(mPreview.isVideo()); + + focus_value = mPreview.getCameraController().getFocusValue(); + assertTrue(focus_value.equals("focus_mode_auto")); + } + + public void testTakePhotoExposureCompensation() throws InterruptedException { + Log.d(TAG, "testTakePhotoExposureCompensation"); + setToDefault(); + + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureContainer = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_container); + SeekBar seekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_seekbar); + ZoomControls seekBarZoom = (ZoomControls) mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_seekbar_zoom); + assertTrue(exposureButton.getVisibility() == (mPreview.supportsExposures() ? View.VISIBLE : View.GONE)); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue(seekBarZoom.getVisibility() == View.GONE); + + if( !mPreview.supportsExposures() ) { + return; + } + + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(seekBarZoom.getVisibility() == View.VISIBLE); + + assertTrue( mPreview.getMaximumExposure() - mPreview.getMinimumExposure() == seekBar.getMax() ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + Log.d(TAG, "change exposure to 1"); + mActivity.changeExposure(1); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCurrentExposure() == 1 ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + Log.d(TAG, "set exposure to min"); + seekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "actual exposure is now " + mPreview.getCurrentExposure()); + Log.d(TAG, "expected exposure to be " + mPreview.getMinimumExposure()); + assertTrue( mPreview.getCurrentExposure() == mPreview.getMinimumExposure() ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + + // test the exposure button clears and reopens without changing exposure level + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue(seekBarZoom.getVisibility() == View.GONE); + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(seekBarZoom.getVisibility() == View.VISIBLE); + assertTrue( mPreview.getCurrentExposure() == mPreview.getMinimumExposure() ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + + // test touch to focus clears the exposure controls + int [] gui_location = new int[2]; + mPreview.getView().getLocationOnScreen(gui_location); + final int step_dist_c = 2; + final int step_count_c = 10; + TouchUtils.drag(MainActivityTest.this, gui_location[0]+step_dist_c, gui_location[0], gui_location[1]+step_dist_c, gui_location[1], step_count_c); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue(seekBarZoom.getVisibility() == View.GONE); + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(seekBarZoom.getVisibility() == View.VISIBLE); + assertTrue( mPreview.getCurrentExposure() == mPreview.getMinimumExposure() ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + + Log.d(TAG, "set exposure to -1"); + seekBar.setProgress(-1 - mPreview.getMinimumExposure()); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCurrentExposure() == -1 ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + + // clear again so as to not interfere with take photo routine + TouchUtils.drag(MainActivityTest.this, gui_location[0]+step_dist_c, gui_location[0], gui_location[1]+step_dist_c, gui_location[1], step_count_c); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue(seekBarZoom.getVisibility() == View.GONE); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue(seekBarZoom.getVisibility() == View.GONE); + assertTrue( mPreview.getCurrentExposure() == -1 ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(seekBarZoom.getVisibility() == View.VISIBLE); + assertTrue( mPreview.getCurrentExposure() == -1 ); + assertTrue( mPreview.getCurrentExposure() - mPreview.getMinimumExposure() == seekBar.getProgress() ); + } + } + + public void testExposureLockNotSaved() { + Log.d(TAG, "testExposureLockNotSaved"); + + if( !mPreview.supportsExposureLock() ) { + return; + } + + setToDefault(); + + View exposureLockButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + clickView(exposureLockButton); + assertTrue(mPreview.getCameraController().getAutoExposureLock()); + + this.pauseAndResume(); + assertTrue(!mPreview.getCameraController().getAutoExposureLock()); + + // now with restart + + clickView(exposureLockButton); + assertTrue(mPreview.getCameraController().getAutoExposureLock()); + + restart(); + assertTrue(!mPreview.getCameraController().getAutoExposureLock()); + } + + public void testTakePhotoManualISOExposure() throws InterruptedException { + Log.d(TAG, "testTakePhotoManualISOExposure"); + if( !mPreview.usingCamera2API() ) { + return; + } + else if( !mPreview.supportsISORange() ) { + return; + } + setToDefault(); + + switchToISO(100); + + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureContainer = mActivity.findViewById(net.sourceforge.opencamera.R.id.manual_exposure_container); + SeekBar isoSeekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.iso_seekbar); + SeekBar exposureTimeSeekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_time_seekbar); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(isoSeekBar.getVisibility() == View.VISIBLE); + assertTrue(exposureTimeSeekBar.getVisibility() == (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); + + assertTrue( isoSeekBar.getMax() == 100 ); + if( mPreview.supportsExposureTime() ) + assertTrue( exposureTimeSeekBar.getMax() == 100 ); + + Log.d(TAG, "change ISO to min"); + isoSeekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMinimumISO() ); + + if( mPreview.supportsExposureTime() ) { + Log.d(TAG, "change exposure time to min"); + exposureTimeSeekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMinimumISO() ); + assertTrue( mPreview.getCameraController().getExposureTime() == mPreview.getMinimumExposureTime() ); + } + + Log.d(TAG, "change ISO to max"); + isoSeekBar.setProgress(100); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + + // n.b., currently don't test this on devices with long shutter times (e.g., OnePlus 3T) until this is properly supported + if( mPreview.supportsExposureTime() && mPreview.getMaximumExposureTime() < 1000000000 ) { + Log.d(TAG, "change exposure time to max"); + exposureTimeSeekBar.setProgress(100); + this.getInstrumentation().waitForIdleSync(); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + assertTrue( mPreview.getCameraController().getExposureTime() == mPreview.getMaximumExposureTime() ); + } + long saved_exposure_time = mPreview.getCameraController().getExposureTime(); + + // test the exposure button clears and reopens without changing exposure level + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(isoSeekBar.getVisibility() == View.VISIBLE); + assertTrue(exposureTimeSeekBar.getVisibility() == (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + if( mPreview.supportsExposureTime() ) + assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + + // test touch to focus clears the exposure controls + int [] gui_location = new int[2]; + mPreview.getView().getLocationOnScreen(gui_location); + final int step_dist_c = 2; + final int step_count_c = 10; + TouchUtils.drag(MainActivityTest.this, gui_location[0]+step_dist_c, gui_location[0], gui_location[1]+step_dist_c, gui_location[1], step_count_c); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(isoSeekBar.getVisibility() == View.VISIBLE); + assertTrue(exposureTimeSeekBar.getVisibility() == (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + if( mPreview.supportsExposureTime() ) + assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + + // clear again so as to not interfere with take photo routine + TouchUtils.drag(MainActivityTest.this, gui_location[0]+step_dist_c, gui_location[0], gui_location[1]+step_dist_c, gui_location[1], step_count_c); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.GONE); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + if( mPreview.supportsExposureTime() ) { + Log.d(TAG, "exposure time: " + mPreview.getCameraController().getExposureTime()); + Log.d(TAG, "min exposure time: " + mPreview.getMinimumExposureTime()); + Log.d(TAG, "max exposure time: " + mPreview.getMaximumExposureTime()); + if( saved_exposure_time < mPreview.getMinimumExposureTime() ) + saved_exposure_time = mPreview.getMinimumExposureTime(); + if( saved_exposure_time > mPreview.getMaximumExposureTime() ) + saved_exposure_time = mPreview.getMaximumExposureTime(); + assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + } + + clickView(exposureButton); + assertTrue(exposureButton.getVisibility() == View.VISIBLE); + assertTrue(exposureContainer.getVisibility() == View.VISIBLE); + assertTrue(isoSeekBar.getVisibility() == View.VISIBLE); + assertTrue(exposureTimeSeekBar.getVisibility() == (mPreview.supportsExposureTime() ? View.VISIBLE : View.GONE)); + assertTrue( mPreview.getCameraController().getISO() == mPreview.getMaximumISO() ); + if( mPreview.supportsExposureTime() ) + assertTrue( mPreview.getCameraController().getExposureTime() == saved_exposure_time ); + } + } + + /** Tests that the audio control icon is visible or not as expect (guards against bug fixed in 1.30) + */ + public void testAudioControlIcon() { + Log.d(TAG, "testAudioControlIcon"); + + setToDefault(); + + View audioControlButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + assertTrue( audioControlButton.getVisibility() == View.GONE ); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getAudioControlPreferenceKey(), "noise"); + editor.apply(); + updateForSettings(); + assertTrue( audioControlButton.getVisibility() == View.VISIBLE ); + + restart(); + // reset due to restarting! + settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + editor = settings.edit(); + audioControlButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + + assertTrue( audioControlButton.getVisibility() == View.VISIBLE ); + + editor.putString(PreferenceKeys.getAudioControlPreferenceKey(), "none"); + editor.apply(); + updateForSettings(); + Log.d(TAG, "visibility is now: " + audioControlButton.getVisibility()); + assertTrue( audioControlButton.getVisibility() == View.GONE ); + + editor.putString(PreferenceKeys.getAudioControlPreferenceKey(), "voice"); + editor.apply(); + updateForSettings(); + assertTrue( audioControlButton.getVisibility() == View.VISIBLE ); + + editor.putString(PreferenceKeys.getAudioControlPreferenceKey(), "none"); + editor.apply(); + updateForSettings(); + Log.d(TAG, "visibility is now: " + audioControlButton.getVisibility()); + assertTrue( audioControlButton.getVisibility() == View.GONE ); + } + + /* + * Note that we pass test_wait_capture_result as a parameter rather than reading from the activity, as for some reason this sometimes resets to false?! Declaring it volatile doesn't fix the problem. + */ + private void subTestTakePhoto(boolean locked_focus, boolean immersive_mode, boolean touch_to_focus, boolean wait_after_focus, boolean single_tap_photo, boolean double_tap_photo, boolean is_raw, boolean test_wait_capture_result) throws InterruptedException { + assertTrue(mPreview.isPreviewStarted()); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity); + boolean has_thumbnail_anim = sharedPreferences.getBoolean(PreferenceKeys.getThumbnailAnimationPreferenceKey(), true); + boolean has_audio_control_button = !sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("none"); + boolean is_dro = mActivity.supportsDRO() && sharedPreferences.getString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std").equals("preference_photo_mode_dro"); + boolean is_hdr = mActivity.supportsHDR() && sharedPreferences.getString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std").equals("preference_photo_mode_hdr"); + boolean hdr_save_expo = sharedPreferences.getBoolean(PreferenceKeys.getHDRSaveExpoPreferenceKey(), false); + boolean is_expo = mActivity.supportsHDR() && sharedPreferences.getString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std").equals("preference_photo_mode_expo_bracketing"); + String n_expo_images_s = sharedPreferences.getString(PreferenceKeys.getExpoBracketingNImagesPreferenceKey(), "3"); + int n_expo_images = Integer.parseInt(n_expo_images_s); + + int saved_count_cameraTakePicture = mPreview.count_cameraTakePicture; + + // count initial files in folder + File folder = mActivity.getImageFolder(); + Log.d(TAG, "folder: " + folder); + File [] files = folder.listFiles(); + int n_files = files.length; + Log.d(TAG, "n_files at start: " + n_files); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + //View flashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.flash); + //View focusButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View audioControlButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + View trashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.trash); + View shareButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.share); + assertTrue(switchCameraButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + int exposureVisibility = exposureButton.getVisibility(); + int exposureLockVisibility = exposureLockButton.getVisibility(); + assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + String focus_value = mPreview.getCameraController().getFocusValue(); + String focus_value_ui = mPreview.getCurrentFocusValue(); + boolean can_auto_focus = false; + boolean manual_can_auto_focus = false; + boolean can_focus_area = false; + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") ) { + can_auto_focus = true; + } + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") || focus_value.equals("focus_mode_continuous_picture") ) { + manual_can_auto_focus = true; + } + if( mPreview.getMaxNumFocusAreas() != 0 && ( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") || focus_value.equals("focus_mode_continuous_picture") || focus_value.equals("focus_mode_continuous_video") || focus_value.equals("focus_mode_manual2") ) ) { + can_focus_area = true; + } + Log.d(TAG, "focus_value? " + focus_value); + Log.d(TAG, "can_auto_focus? " + can_auto_focus); + Log.d(TAG, "manual_can_auto_focus? " + manual_can_auto_focus); + Log.d(TAG, "can_focus_area? " + can_focus_area); + int saved_count = mPreview.count_cameraAutoFocus; + String new_focus_value_ui = mPreview.getCurrentFocusValue(); + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + if( touch_to_focus ) { + // touch to auto-focus with focus area (will also exit immersive mode) + // autofocus shouldn't be immediately, but after a delay + Thread.sleep(1000); + saved_count = mPreview.count_cameraAutoFocus; + Log.d(TAG, "saved count_cameraAutoFocus: " + saved_count); + Log.d(TAG, "about to click preview for autofocus"); + if( double_tap_photo ) { + TouchUtils.tapView(MainActivityTest.this, mPreview.getView()); + } + else { + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "1 count_cameraAutoFocus: " + mPreview.count_cameraAutoFocus); + assertTrue(mPreview.count_cameraAutoFocus == (manual_can_auto_focus ? saved_count+1 : saved_count)); + assertTrue(mPreview.hasFocusArea() == can_focus_area); + if( can_focus_area ) { + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + else { + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + // we still set metering areas + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + new_focus_value_ui = mPreview.getCurrentFocusValue(); + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + if( focus_value.equals("focus_mode_continuous_picture") ) + assertTrue(mPreview.getCameraController().getFocusValue().equals("focus_mode_auto")); // continuous focus mode switches to auto focus on touch + else + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + if( double_tap_photo ) { + Thread.sleep(100); + Log.d(TAG, "about to click preview again for double tap"); + //TouchUtils.tapView(MainActivityTest.this, mPreview.getView()); + mPreview.onDoubleTap(); // calling tapView twice doesn't seem to work consistently, so we call this directly! + this.getInstrumentation().waitForIdleSync(); + } + if( wait_after_focus && !single_tap_photo && !double_tap_photo) { + // don't wait after single or double tap photo taking, as the photo taking operation is already started + Log.d(TAG, "wait after focus..."); + Thread.sleep(3000); + } + } + Log.d(TAG, "saved count_cameraAutoFocus: " + saved_count); + + if( !single_tap_photo && !double_tap_photo ) { + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + assertFalse( mActivity.hasThumbnailAnimation() ); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + } + + Log.d(TAG, "wait until finished taking photo"); + long time_s = System.currentTimeMillis(); + while( mPreview.isTakingPhoto() ) { + assertTrue( System.currentTimeMillis() - time_s < 20000 ); // make sure the test fails rather than hanging, if for some reason we get stuck (note that testTakePhotoManualISOExposure takes over 10s on Nexus 6) + assertTrue(!mPreview.isTakingPhoto() || switchCameraButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || switchVideoButton.getVisibility() == View.GONE); + //assertTrue(!mPreview.isTakingPhoto() || flashButton.getVisibility() == View.GONE); + //assertTrue(!mPreview.isTakingPhoto() || focusButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || exposureButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || exposureLockButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || audioControlButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || popupButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || trashButton.getVisibility() == View.GONE); + assertTrue(!mPreview.isTakingPhoto() || shareButton.getVisibility() == View.GONE); + } + Log.d(TAG, "done taking photo"); + + Date date = new Date(); + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(date); + String suffix = ""; + if( is_dro ) { + suffix = "_DRO"; + } + else if( is_hdr ) { + suffix = "_HDR"; + } + else if( is_expo ) { + suffix = "_EXP" + (n_expo_images-1); + } + String expected_filename = "IMG_" + timeStamp + suffix + ".jpg"; + // allow for possibility that the time has passed on by 1s since taking the photo + Date date1 = new Date(date.getTime() - 1000); + String timeStamp1 = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(date1); + String expected_filename1= "IMG_" + timeStamp1 + suffix + ".jpg"; + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + Log.d(TAG, "take picture count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==saved_count_cameraTakePicture+1); + if( test_wait_capture_result ) { + // if test_wait_capture_result, then we'll have waited too long for thumbnail animation + } + else if( has_thumbnail_anim ) { + while( !mActivity.hasThumbnailAnimation() ) { + Log.d(TAG, "waiting for thumbnail animation"); + Thread.sleep(10); + } + } + else { + assertFalse( mActivity.hasThumbnailAnimation() ); + } + mActivity.waitUntilImageQueueEmpty(); + Log.d(TAG, "mActivity.hasThumbnailAnimation()?: " + mActivity.hasThumbnailAnimation()); + + // focus should be back to normal now: + new_focus_value_ui = mPreview.getCurrentFocusValue(); + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + Log.d(TAG, "focus_value: " + focus_value); + Log.d(TAG, "new focus_value: " + mPreview.getCameraController().getFocusValue()); + assertTrue(mPreview.getCameraController().getFocusValue().equals(focus_value)); + + assertTrue( folder.exists() ); + File [] files2 = folder.listFiles(); + int n_new_files = files2.length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + int exp_n_new_files; + if( is_raw ) + exp_n_new_files = 2; + else if( is_hdr && hdr_save_expo ) + exp_n_new_files = 4; + else if( is_expo ) + exp_n_new_files = n_expo_images; + else + exp_n_new_files = 1; + Log.d(TAG, "exp_n_new_files: " + exp_n_new_files); + assertTrue(n_new_files == exp_n_new_files); + // check files have names as expected + String filename_jpeg = null; + String filename_dng = null; + for(File file : files2) { + Log.d(TAG, "file: " + file); + boolean is_new = true; + for(int j=0;j 1 ) { + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + takePhotoLoop(n_photos_c); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + } + + mActivity.test_low_memory = true; + + takePhotoLoop(n_photos_c); + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + takePhotoLoop(n_photos_c); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + } + } + + private void takePhotoLoopAngles(int [] angles) { + // count initial files in folder + mActivity.test_have_angle = true; + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + int start_count = mPreview.count_cameraTakePicture; + for(int i=0;i 1 ) { + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + takePhotoLoopAngles(angles); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + } + + mActivity.test_low_memory = true; + + takePhotoLoopAngles(angles); + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + takePhotoLoopAngles(angles); + while( switchCameraButton.getVisibility() != View.VISIBLE ) { + // wait until photo is taken and button is visible again + } + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + } + } + + private interface VideoTestCallback { + int doTest(); // return expected number of new files (or -1 to indicate not to check this) + } + + private void subTestTakeVideo(boolean test_exposure_lock, boolean test_focus_area, boolean allow_failure, boolean immersive_mode, VideoTestCallback test_cb, long time_ms, boolean max_filesize, boolean subtitles) throws InterruptedException { + assertTrue(mPreview.isPreviewStarted()); + + if( test_exposure_lock && !mPreview.supportsExposureLock() ) { + return; + } + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + View pauseVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.pause_video); + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + if( mPreview.isVideo() ) { + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_photo) ) ); + } + else { + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_photo_selector ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.take_photo) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_video) ) ); + } + assertTrue( pauseVideoButton.getVisibility() == View.INVISIBLE ); + + if( !mPreview.isVideo() ) { + clickView(switchVideoButton); + } + assertTrue(mPreview.isVideo()); + assertTrue(mPreview.isPreviewStarted()); + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( switchVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.switch_to_photo) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.INVISIBLE ); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + Log.d(TAG, "folder: " + folder); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity); + boolean has_audio_control_button = !sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("none"); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + //View flashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.flash); + //View focusButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View audioControlButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + View trashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.trash); + View shareButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.share); + assertTrue(switchCameraButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + // but store status to compare with later + int exposureVisibility = exposureButton.getVisibility(); + int exposureLockVisibility = exposureLockButton.getVisibility(); + assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ) ); + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + int exp_n_new_files = 0; + if( mPreview.isTakingPhoto() ) { + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_recording ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + else + assertTrue( pauseVideoButton.getVisibility() == View.INVISIBLE ); + assertTrue(switchCameraButton.getVisibility() == View.GONE); + assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(audioControlButton.getVisibility() == View.GONE); + assertTrue(popupButton.getVisibility() == (!immersive_mode && mPreview.supportsFlash() ? View.VISIBLE : View.GONE)); // popup button only visible when recording video if flash supported + assertTrue(exposureButton.getVisibility() == exposureVisibility); + assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + if( test_cb == null ) { + if( !immersive_mode && time_ms > 500 ) { + // test turning torch on/off (if in immersive mode, popup button will be hidden) + switchToFlashValue("flash_torch"); + Thread.sleep(500); + switchToFlashValue("flash_off"); + } + + Thread.sleep(time_ms); + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_recording ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + + assertTrue(!mPreview.hasFocusArea()); + if( !allow_failure ) { + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + } + + if( test_focus_area ) { + // touch to auto-focus with focus area + Log.d(TAG, "touch to focus"); + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + Thread.sleep(1000); // wait for autofocus + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + Log.d(TAG, "done touch to focus"); + + // this time, don't wait + Log.d(TAG, "touch again to focus"); + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + } + + if( test_exposure_lock ) { + Log.d(TAG, "test exposure lock"); + assertTrue( !mPreview.getCameraController().getAutoExposureLock() ); + clickView(exposureLockButton); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue( mPreview.getCameraController().getAutoExposureLock() ); + Thread.sleep(2000); + } + + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_recording ); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + Log.d(TAG, "about to click stop video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking stop video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + } + else { + exp_n_new_files = test_cb.doTest(); + } + } + else { + Log.d(TAG, "didn't start video"); + assertTrue(allow_failure); + } + + assertTrue( folder.exists() ); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + if( test_cb == null ) { + if( time_ms <= 500 ) { + // if quick, should have deleted corrupt video - but may be device dependent, sometimes we manage to record a video anyway! + assertTrue(n_new_files == 0 || n_new_files == 1); + } + else if( subtitles ) { + assertEquals(n_new_files, 2); + } + else { + assertEquals(n_new_files, 1); + } + } + else { + Log.d(TAG, "exp_n_new_files: " + exp_n_new_files); + if( exp_n_new_files >= 0 ) { + assertEquals(n_new_files, exp_n_new_files); + } + } + + // trash/share only shown when preview is paused after taking a photo + + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + if( !max_filesize ) { + // if doing restart on max filesize, we may have already restarted by now (on Camera2 API at least) + assertTrue(switchCameraButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(audioControlButton.getVisibility() == ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + } + assertTrue(switchVideoButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(exposureButton.getVisibility() == exposureVisibility); + assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertTrue(popupButton.getVisibility() == (immersive_mode ? View.GONE : View.VISIBLE)); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + assertTrue( (Integer)takePhotoButton.getTag() == net.sourceforge.opencamera.R.drawable.take_video_selector ); + assertEquals( takePhotoButton.getContentDescription(), mActivity.getResources().getString(net.sourceforge.opencamera.R.string.start_video) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + Log.d(TAG, "pauseVideoButton.getVisibility(): " + pauseVideoButton.getVisibility()); + assertTrue( pauseVideoButton.getVisibility() == View.INVISIBLE ); + } + + public void testTakeVideo() throws InterruptedException { + Log.d(TAG, "testTakeVideo"); + + setToDefault(); + + subTestTakeVideo(false, false, false, false, null, 5000, false, false); + } + + public void testTakeVideoAudioControl() throws InterruptedException { + Log.d(TAG, "testTakeVideoAudioControl"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getAudioControlPreferenceKey(), "voice"); + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, false, false, null, 5000, false, false); + } + + // If this test fails, make sure we've manually selected that folder (as permission can't be given through the test framework). + public void testTakeVideoSAF() throws InterruptedException { + Log.d(TAG, "testTakeVideoSAF"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + Log.d(TAG, "SAF requires Android Lollipop or better"); + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), true); + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), "content://com.android.externalstorage.documents/tree/primary%3ADCIM%2FOpenCamera"); + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, false, false, null, 5000, false, false); + } + + public void testTakeVideoSubtitles() throws InterruptedException { + Log.d(TAG, "testTakeVideoSubtitles"); + + setToDefault(); + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoSubtitlePref(), "preference_video_subtitle_yes"); + editor.apply(); + updateForSettings(); + } + + subTestTakeVideo(false, false, false, false, null, 5000, false, true); + } + + /** Set pausing and resuming video. + */ + public void testTakeVideoPause() throws InterruptedException { + Log.d(TAG, "testTakeVideoPause"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.N ) { + Log.d(TAG, "pause video requires Android N or better"); + return; + } + + setToDefault(); + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + View pauseVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.pause_video); + final long time_tol_ms = 1000; + + Log.d(TAG, "wait before pausing"); + try { + Thread.sleep(3000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( !mPreview.isVideoRecordingPaused() ); + long video_time = mPreview.getVideoTime(); + Log.d(TAG, "video time: " + video_time); + assertTrue( video_time >= 3000 - time_tol_ms ); + assertTrue( video_time <= 3000 + time_tol_ms ); + + Log.d(TAG, "about to click pause video"); + clickView(pauseVideoButton); + Log.d(TAG, "done clicking pause video"); + getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.resume_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( mPreview.isVideoRecordingPaused() ); + + Log.d(TAG, "wait before resuming"); + try { + Thread.sleep(3000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.resume_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( mPreview.isVideoRecordingPaused() ); + video_time = mPreview.getVideoTime(); + Log.d(TAG, "video time: " + video_time); + assertTrue( video_time >= 3000 - time_tol_ms ); + assertTrue( video_time <= 3000 + time_tol_ms ); + + Log.d(TAG, "about to click resume video"); + clickView(pauseVideoButton); + Log.d(TAG, "done clicking resume video"); + getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( !mPreview.isVideoRecordingPaused() ); + + Log.d(TAG, "wait before stopping"); + try { + Thread.sleep(3000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + Log.d(TAG, "takePhotoButton description: " + takePhotoButton.getContentDescription()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( !mPreview.isVideoRecordingPaused() ); + video_time = mPreview.getVideoTime(); + Log.d(TAG, "video time: " + video_time); + assertTrue( video_time >= 6000 - time_tol_ms ); + assertTrue( video_time <= 6000 + time_tol_ms ); + + Log.d(TAG, "about to click stop video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking stop video"); + getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + return 1; + } + }, 5000, false, false); + } + + /** Set pausing and stopping video. + */ + public void testTakeVideoPauseStop() throws InterruptedException { + Log.d(TAG, "testTakeVideoPauseStop"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.N ) { + Log.d(TAG, "pause video requires Android N or better"); + return; + } + + setToDefault(); + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + View pauseVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.pause_video); + final long time_tol_ms = 1000; + + Log.d(TAG, "wait before pausing"); + try { + Thread.sleep(3000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.pause_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( !mPreview.isVideoRecordingPaused() ); + long video_time = mPreview.getVideoTime(); + Log.d(TAG, "video time: " + video_time); + assertTrue( video_time >= 3000 - time_tol_ms ); + assertTrue( video_time <= 3000 + time_tol_ms ); + + Log.d(TAG, "about to click pause video"); + clickView(pauseVideoButton); + Log.d(TAG, "done clicking pause video"); + getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.resume_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( mPreview.isVideoRecordingPaused() ); + + Log.d(TAG, "wait before stopping"); + try { + Thread.sleep(3000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + Log.d(TAG, "takePhotoButton description: " + takePhotoButton.getContentDescription()); + assertTrue( takePhotoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.stop_video) ) ); + assertTrue( pauseVideoButton.getContentDescription().equals( mActivity.getResources().getString(net.sourceforge.opencamera.R.string.resume_video) ) ); + assertTrue( pauseVideoButton.getVisibility() == View.VISIBLE ); + assertTrue( mPreview.isVideoRecording() ); + assertTrue( mPreview.isVideoRecordingPaused() ); + video_time = mPreview.getVideoTime(); + Log.d(TAG, "video time: " + video_time); + assertTrue( video_time >= 3000 - time_tol_ms ); + assertTrue( video_time <= 3000 + time_tol_ms ); + + Log.d(TAG, "about to click stop video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking stop video"); + getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + return 1; + } + }, 5000, false, false); + } + + /** Set available memory to make sure that we stop before running out of memory. + * This test is fine-tuned to Nexus 6, as we measure hitting max filesize based on time. + */ + public void testTakeVideoAvailableMemory() throws InterruptedException { + Log.d(TAG, "testTakeVideoAvailableMemory"); + + setToDefault(); + + mActivity.getApplicationInterface().test_set_available_memory = true; + mActivity.getApplicationInterface().test_available_memory = 50000000; + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + // wait until automatically stops + Log.d(TAG, "wait until video recording stops"); + long time_s = System.currentTimeMillis(); + long video_time_s = mPreview.getVideoTime(); + while( mPreview.isVideoRecording() ) { + assertTrue( System.currentTimeMillis() - time_s <= 30000 ); + long video_time = mPreview.getVideoTime(); + assertTrue( video_time >= video_time_s ); + } + Log.d(TAG, "video recording now stopped"); + return 1; + } + }, 5000, true, false); + } + + /** Set available memory small enough to make sure we don't even attempt to record video. + */ + public void testTakeVideoAvailableMemory2() throws InterruptedException { + Log.d(TAG, "testTakeVideoAvailableMemory2"); + + setToDefault(); + + mActivity.getApplicationInterface().test_set_available_memory = true; + mActivity.getApplicationInterface().test_available_memory = 5000000; + + subTestTakeVideo(false, false, true, false, new VideoTestCallback() { + @Override + public int doTest() { + // wait until automatically stops + Log.d(TAG, "wait until video recording stops"); + assertFalse( mPreview.isVideoRecording() ); + Log.d(TAG, "video recording now stopped"); + return 0; + } + }, 5000, true, false); + } + + /** Set maximum filesize so that we get approx 3s of video time. Check that recording stops and restarts within 10s. + * Then check recording stops again within 10s. + * This test is fine-tuned to Nexus 6, as we measure hitting max filesize based on time. + */ + public void testTakeVideoMaxFileSize1() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxFileSize1"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + //editor.putString(PreferenceKeys.getVideoQualityPreferenceKey(mPreview.getCameraId()), "" + CamcorderProfile.QUALITY_HIGH); // set to highest quality (4K on Nexus 6) + //editor.putString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "15728640"); // approx 3-4s on Nexus 6 at 4K + editor.putString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "9437184"); // approx 3-4s on Nexus 6 at 4K + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + // wait until automatically stops + Log.d(TAG, "wait until video recording stops"); + long time_s = System.currentTimeMillis(); + long video_time_s = mPreview.getVideoTime(); + while( mPreview.isVideoRecording() ) { + assertTrue( System.currentTimeMillis() - time_s <= 8000 ); + long video_time = mPreview.getVideoTime(); + assertTrue( video_time >= video_time_s ); + } + Log.d(TAG, "video recording now stopped - wait for restart"); + video_time_s = mPreview.getVideoAccumulatedTime(); + Log.d(TAG, "video_time_s: " + video_time_s); + // now ensure we'll restart within a reasonable time + time_s = System.currentTimeMillis(); + while( !mPreview.isVideoRecording() ) { + long c_time = System.currentTimeMillis(); + if( c_time - time_s > 10000 ) { + Log.e(TAG, "time: " + (c_time - time_s)); + } + assertTrue( c_time - time_s <= 10000 ); + } + // wait for stop again + time_s = System.currentTimeMillis(); + while( mPreview.isVideoRecording() ) { + long c_time = System.currentTimeMillis(); + if( c_time - time_s > 10000 ) { + Log.e(TAG, "time: " + (c_time - time_s)); + } + assertTrue( c_time - time_s <= 10000 ); + long video_time = mPreview.getVideoTime(); + if( video_time < video_time_s ) + Log.d(TAG, "compare: " + video_time_s + " to " + video_time); + assertTrue( video_time + 1 >= video_time_s ); + } + Log.d(TAG, "video recording now stopped again"); + return -1; // the number of videos recorded can very, as the max duration corresponding to max filesize can vary widly + } + }, 5000, true, false); + } + + /** Max filesize is for ~4.5s, and max duration is 5s, check we only get 1 video. + * This test is fine-tuned to Nexus 6, as we measure hitting max filesize based on time. + */ + public void testTakeVideoMaxFileSize2() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxFileSize2"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoQualityPreferenceKey(mPreview.getCameraId()), "" + CamcorderProfile.QUALITY_HIGH); // set to highest quality (4K on Nexus 6) + editor.putString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "23592960"); // approx 4.5s on Nexus 6 at 4K + editor.putString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "5"); + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + // wait until automatically stops + Log.d(TAG, "wait until video recording stops"); + long time_s = System.currentTimeMillis(); + long video_time_s = mPreview.getVideoTime(); + while( mPreview.isVideoRecording() ) { + assertTrue( System.currentTimeMillis() - time_s <= 8000 ); + long video_time = mPreview.getVideoTime(); + assertTrue( video_time >= video_time_s ); + } + Log.d(TAG, "video recording now stopped - check we don't restart"); + video_time_s = mPreview.getVideoAccumulatedTime(); + Log.d(TAG, "video_time_s: " + video_time_s); + // now ensure we don't restart + time_s = System.currentTimeMillis(); + while( System.currentTimeMillis() - time_s <= 5000 ) { + assertFalse( mPreview.isVideoRecording() ); + } + return 1; + } + }, 5000, true, false); + } + + /* Max filesize for ~5s, max duration 7s, max n_repeats 1 - to ensure we're not repeating indefinitely. + * This test is fine-tuned to Nexus 6, as we measure hitting max filesize based on time. + */ + public void testTakeVideoMaxFileSize3() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxFileSize3"); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoQualityPreferenceKey(mPreview.getCameraId()), "" + CamcorderProfile.QUALITY_HIGH); // set to highest quality (4K on Nexus 6) + //editor.putString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "26214400"); // approx 5s on Nexus 6 at 4K + editor.putString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "15728640"); // approx 5s on Nexus 6 at 4K + editor.putString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "7"); + editor.putString(PreferenceKeys.getVideoRestartPreferenceKey(), "1"); + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, false, false, new VideoTestCallback() { + @Override + public int doTest() { + // wait until we should have stopped - 2x7s, but add 6s for each of 4 restarts + Log.d(TAG, "wait until video recording completely stopped"); + try { + Thread.sleep(38000); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + Log.d(TAG, "ensure we've really stopped"); + long time_s = System.currentTimeMillis(); + while( System.currentTimeMillis() - time_s <= 5000 ) { + assertFalse( mPreview.isVideoRecording() ); + } + return -1; // the number of videos recorded can very, as the max duration corresponding to max filesize can vary widly + } + }, 5000, true, false); + } + + public void testTakeVideoStabilization() throws InterruptedException { + Log.d(TAG, "testTakeVideoStabilization"); + + if( !mPreview.supportsVideoStabilization() ) { + Log.d(TAG, "video stabilization not supported"); + return; + } + assertFalse(mPreview.getCameraController().getVideoStabilization()); + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getVideoStabilizationPreferenceKey(), true); + editor.apply(); + updateForSettings(); + assertTrue(mPreview.getCameraController().getVideoStabilization()); + + subTestTakeVideo(false, false, false, false, null, 5000, false, false); + + assertTrue(mPreview.getCameraController().getVideoStabilization()); + } + + public void testTakeVideoExposureLock() throws InterruptedException { + Log.d(TAG, "testTakeVideoExposureLock"); + + setToDefault(); + + subTestTakeVideo(true, false, false, false, null, 5000, false, false); + } + + public void testTakeVideoFocusArea() throws InterruptedException { + Log.d(TAG, "testTakeVideoFocusArea"); + + setToDefault(); + + subTestTakeVideo(false, true, false, false, null, 5000, false, false); + } + + public void testTakeVideoQuick() throws InterruptedException { + Log.d(TAG, "testTakeVideoQuick"); + + setToDefault(); + + // still need a short delay (at least 500ms, otherwise Open Camera will ignore the repeated stop) + subTestTakeVideo(false, false, false, false, null, 500, false, false); + } + + // If this test fails, make sure we've manually selected that folder (as permission can't be given through the test framework). + public void testTakeVideoQuickSAF() throws InterruptedException { + Log.d(TAG, "testTakeVideoQuickSAF"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + Log.d(TAG, "SAF requires Android Lollipop or better"); + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), true); + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), "content://com.android.externalstorage.documents/tree/primary%3ADCIM%2FOpenCamera"); + editor.apply(); + updateForSettings(); + + // still need a short delay (at least 500ms, otherwise Open Camera will ignore the repeated stop) + subTestTakeVideo(false, false, false, false, null, 500, false, false); + } + + public void testTakeVideoForceFailure() throws InterruptedException { + Log.d(TAG, "testTakeVideoForceFailure"); + + setToDefault(); + + mActivity.getPreview().test_video_failure = true; + subTestTakeVideo(false, false, true, false, null, 5000, false, false); + } + + /* Test can be reliable on some devices, test no longer run as part of test suites. + */ + public void testTakeVideo4K() throws InterruptedException { + Log.d(TAG, "testTakeVideo4K"); + + if( !mActivity.supportsForceVideo4K() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getForceVideo4KPreferenceKey(), true); + editor.apply(); + updateForSettings(); + + subTestTakeVideo(false, false, true, false, null, 5000, false, false); + } + + /* Test can be reliable on some devices, test no longer run as part of test suites. + */ + public void testTakeVideoFPS() throws InterruptedException { + Log.d(TAG, "testTakeVideoFPS"); + + setToDefault(); + final String [] fps_values = new String[]{"15", "24", "25", "30", "60"}; + for(String fps_value : fps_values) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoFPSPreferenceKey(), fps_value); + editor.apply(); + restart(); // should restart to emulate what happens in real app + + Log.d(TAG, "test video with fps: " + fps_value); + boolean allow_failure = fps_value.equals("24") || fps_value.equals("25") || fps_value.equals("60"); + subTestTakeVideo(false, false, allow_failure, false, null, 5000, false, false); + } + } + + /* Test can be reliable on some devices, test no longer run as part of test suites. + */ + public void testTakeVideoBitrate() throws InterruptedException { + Log.d(TAG, "testTakeVideoBitrate"); + + setToDefault(); + final String [] bitrate_values = new String[]{"1000000", "10000000", "20000000", "50000000"}; + //final String [] bitrate_values = new String[]{"1000000", "10000000", "20000000", "30000000"}; + for(String bitrate_value : bitrate_values) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoBitratePreferenceKey(), bitrate_value); + editor.apply(); + restart(); // should restart to emulate what happens in real app + + Log.d(TAG, "test video with bitrate: " + bitrate_value); + boolean allow_failure = bitrate_value.equals("30000000") || bitrate_value.equals("50000000"); + subTestTakeVideo(false, false, allow_failure, false, null, 5000, false, false); + } + } + + private void subTestTakeVideoMaxDuration(boolean restart, boolean interrupt) throws InterruptedException { + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "15"); + if( restart ) { + editor.putString(PreferenceKeys.getVideoRestartPreferenceKey(), "1"); + } + editor.apply(); + } + + assertTrue(mPreview.isPreviewStarted()); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + if( !mPreview.isVideo() ) { + clickView(switchVideoButton); + } + assertTrue(mPreview.isVideo()); + assertTrue(mPreview.isPreviewStarted()); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity); + boolean has_audio_control_button = !sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("none"); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + //View flashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.flash); + //View focusButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + View exposureButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View audioControlButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + View trashButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.trash); + View shareButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.share); + assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); + assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); + // flash and focus etc default visibility tested in another test + // but store status to compare with later + //int flashVisibility = flashButton.getVisibility(); + //int focusVisibility = focusButton.getVisibility(); + int exposureVisibility = exposureButton.getVisibility(); + int exposureLockVisibility = exposureLockButton.getVisibility(); + assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertTrue(popupButton.getVisibility() == View.VISIBLE); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( mPreview.isTakingPhoto() ); + + assertTrue(switchCameraButton.getVisibility() == View.GONE); + assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); + //assertTrue(flashButton.getVisibility() == flashVisibility); + //assertTrue(focusButton.getVisibility() == View.GONE); + assertTrue(exposureButton.getVisibility() == exposureVisibility); + assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertTrue(audioControlButton.getVisibility() == View.GONE); + assertTrue(popupButton.getVisibility() == (mPreview.supportsFlash() ? View.VISIBLE : View.GONE)); // popup button only visible when recording video if flash supported + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + + Thread.sleep(10000); + Log.d(TAG, "check still taking video"); + assertTrue( mPreview.isTakingPhoto() ); + + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + if( restart ) { + if( interrupt ) { + Thread.sleep(5100); + restart(); + Log.d(TAG, "done restart"); + // now wait, and check we don't crash + Thread.sleep(5000); + return; + } + else { + Thread.sleep(10000); + Log.d(TAG, "check restarted video"); + assertTrue( mPreview.isTakingPhoto() ); + assertTrue( folder.exists() ); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + + Thread.sleep(15000); + } + } + else { + Thread.sleep(8000); + } + Log.d(TAG, "check stopped taking video"); + assertTrue( !mPreview.isTakingPhoto() ); + + assertTrue( folder.exists() ); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == (restart ? 2 : 1)); + + // trash/share only shown when preview is paused after taking a photo + + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + assertTrue(switchCameraButton.getVisibility() == View.VISIBLE); + assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); + //assertTrue(flashButton.getVisibility() == flashVisibility); + //assertTrue(focusButton.getVisibility() == focusVisibility); + assertTrue(exposureButton.getVisibility() == exposureVisibility); + assertTrue(exposureLockButton.getVisibility() == exposureLockVisibility); + assertTrue(audioControlButton.getVisibility() == (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertTrue(popupButton.getVisibility() == View.VISIBLE); + assertTrue(trashButton.getVisibility() == View.GONE); + assertTrue(shareButton.getVisibility() == View.GONE); + } + + public void testTakeVideoMaxDuration() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxDuration"); + + setToDefault(); + + subTestTakeVideoMaxDuration(false, false); + } + + public void testTakeVideoMaxDurationRestart() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxDurationRestart"); + + setToDefault(); + + subTestTakeVideoMaxDuration(true, false); + } + + public void testTakeVideoMaxDurationRestartInterrupt() throws InterruptedException { + Log.d(TAG, "testTakeVideoMaxDurationRestartInterrupt"); + + setToDefault(); + + subTestTakeVideoMaxDuration(true, true); + } + + public void testTakeVideoSettings() throws InterruptedException { + Log.d(TAG, "testTakeVideoSettings"); + + setToDefault(); + + assertTrue(mPreview.isPreviewStarted()); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + if( !mPreview.isVideo() ) { + clickView(switchVideoButton); + } + assertTrue(mPreview.isVideo()); + assertTrue(mPreview.isPreviewStarted()); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( mPreview.isTakingPhoto() ); + + Thread.sleep(2000); + Log.d(TAG, "check still taking video"); + assertTrue( mPreview.isTakingPhoto() ); + + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + // now go to settings + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue( !mPreview.isTakingPhoto() ); + + assertTrue( folder.exists() ); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + Thread.sleep(500); + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "on back pressed..."); + mActivity.onBackPressed(); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + Thread.sleep(500); + assertTrue( !mPreview.isTakingPhoto() ); + + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( mPreview.isTakingPhoto() ); + + assertTrue( folder.exists() ); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + + } + + /** Switch to macro focus, go to settings, check switched to continuous mode, leave settings, check back in macro mode, then test recording. + */ + public void testTakeVideoMacro() throws InterruptedException { + Log.d(TAG, "testTakeVideoMacro"); + if( !mPreview.supportsFocus() ) { + return; + } + + setToDefault(); + + assertTrue(mPreview.isPreviewStarted()); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + if( !mPreview.isVideo() ) { + clickView(switchVideoButton); + } + assertTrue(mPreview.isVideo()); + assertTrue(mPreview.isPreviewStarted()); + + switchToFocusValue("focus_mode_macro"); + + // now go to settings + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue( !mPreview.isTakingPhoto() ); + + Thread.sleep(500); + + assertTrue(mPreview.getCurrentFocusValue().equals("focus_mode_macro")); + + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "on back pressed..."); + mActivity.onBackPressed(); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + Thread.sleep(500); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + assertTrue(switchVideoButton.getVisibility() == View.VISIBLE); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( mPreview.isTakingPhoto() ); + + Thread.sleep(2000); + Log.d(TAG, "check still taking video"); + assertTrue( mPreview.isTakingPhoto() ); + + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + } + + public void testTakeVideoFlashVideo() throws InterruptedException { + Log.d(TAG, "testTakeVideoFlashVideo"); + + if( !mPreview.supportsFlash() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getVideoFlashPreferenceKey(), true); + editor.apply(); + updateForSettings(); + + assertTrue(mPreview.isPreviewStarted()); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + if( !mPreview.isVideo() ) { + clickView(switchVideoButton); + } + assertTrue(mPreview.isVideo()); + assertTrue(mPreview.isPreviewStarted()); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + assertTrue( mPreview.isTakingPhoto() ); + + Thread.sleep(1500); + Log.d(TAG, "check still taking video"); + assertTrue( mPreview.isTakingPhoto() ); + + // wait until flash off + long time_s = System.currentTimeMillis(); + for(;;) { + if( !mPreview.getCameraController().getFlashValue().equals("flash_torch") ) { + break; + } + assertTrue( System.currentTimeMillis() - time_s <= 200 ); + } + + // wait until flash on + time_s = System.currentTimeMillis(); + for(;;) { + if( mPreview.getCameraController().getFlashValue().equals("flash_torch") ) { + break; + } + assertTrue( System.currentTimeMillis() - time_s <= 1100 ); + } + + // wait until flash off + time_s = System.currentTimeMillis(); + for(;;) { + if( !mPreview.getCameraController().getFlashValue().equals("flash_torch") ) { + break; + } + assertTrue( System.currentTimeMillis() - time_s <= 200 ); + } + + // wait until flash on + time_s = System.currentTimeMillis(); + for(;;) { + if( mPreview.getCameraController().getFlashValue().equals("flash_torch") ) { + break; + } + assertTrue( System.currentTimeMillis() - time_s <= 1100 ); + } + + Log.d(TAG, "about to click stop video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking stop video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + // test flash now off + assertTrue( !mPreview.getCameraController().getFlashValue().equals("flash_torch") ); + } + + // type: 0 - go to background; 1 - go to settings; 2 - go to popup + private void subTestTimer(int type) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getTimerPreferenceKey(), "10"); + editor.putBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), false); + editor.apply(); + + assertTrue(!mPreview.isOnTimer()); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==0); + + try { + // wait 2s, and check we are still on timer, and not yet taken a photo + Thread.sleep(2000); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==0); + // quit and resume + if( type == 0 ) + restart(); + else if( type == 1 ) { + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "on back pressed..."); + mActivity.onBackPressed(); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + Thread.sleep(500); + } + else { + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + } + takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + // check timer cancelled, and not yet taken a photo + assertTrue(!mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==0); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 0); + + // start timer again + Log.d(TAG, "about to click take photo"); + assertTrue(mPreview.getCameraController() != null); + clickView(takePhotoButton); + assertTrue(mPreview.getCameraController() != null); + Log.d(TAG, "done clicking take photo"); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==0); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 0); + + // wait 15s, and ensure we took a photo + Thread.sleep(15000); + Log.d(TAG, "waited, count now " + mPreview.count_cameraTakePicture); + assertTrue(!mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==1); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + // now set timer to 5s, and turn on pause_preview + editor.putString(PreferenceKeys.getTimerPreferenceKey(), "5"); + editor.putBoolean(PreferenceKeys.getPausePreviewPreferenceKey(), true); + editor.apply(); + + Log.d(TAG, "about to click take photo"); + assertTrue(mPreview.getCameraController() != null); + clickView(takePhotoButton); + assertTrue(mPreview.getCameraController() != null); + Log.d(TAG, "done clicking take photo"); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==1); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 1); + + // wait 10s, and ensure we took a photo + Thread.sleep(10000); + Log.d(TAG, "waited, count now " + mPreview.count_cameraTakePicture); + assertTrue(!mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==2); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + + // now test cancelling + Log.d(TAG, "about to click take photo"); + assertTrue(mPreview.getCameraController() != null); + clickView(takePhotoButton); + assertTrue(mPreview.getCameraController() != null); + Log.d(TAG, "done clicking take photo"); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==2); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + + // wait 2s, and cancel + Thread.sleep(2000); + Log.d(TAG, "about to click take photo to cance"); + assertTrue(mPreview.getCameraController() != null); + clickView(takePhotoButton); + assertTrue(mPreview.getCameraController() != null); + Log.d(TAG, "done clicking take photo to cancel"); + assertTrue(!mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==2); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + + // wait 8s, and ensure we didn't take a photo + Thread.sleep(8000); + Log.d(TAG, "waited, count now " + mPreview.count_cameraTakePicture); + assertTrue(!mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==2); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 2); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + } + + /* Test with 10s timer, start a photo, go to background, then back, then take another photo. We should only take 1 photo - the original countdown should not be active (nor should we crash)! + */ + public void testTimerBackground() { + Log.d(TAG, "testTimerBackground"); + setToDefault(); + + subTestTimer(0); + } + + /* Test and going to settings. + */ + public void testTimerSettings() { + Log.d(TAG, "testTimerSettings"); + setToDefault(); + + subTestTimer(1); + } + + /* Test and going to popup. + */ + public void testTimerPopup() { + Log.d(TAG, "testTimerPopup"); + setToDefault(); + + subTestTimer(2); + } + + /* Takes video on a timer, but interrupts with restart. + */ + public void testVideoTimerInterrupt() { + Log.d(TAG, "testVideoTimerInterrupt"); + setToDefault(); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getTimerPreferenceKey(), "5"); + editor.putBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), false); + editor.apply(); + + assertTrue(!mPreview.isOnTimer()); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + assertTrue(mPreview.isOnTimer()); + assertTrue(mPreview.count_cameraTakePicture==0); + + try { + // wait a moment after 5s, then restart + Thread.sleep(5100); + assertTrue(mPreview.count_cameraTakePicture==0); + // quit and resume + restart(); + Log.d(TAG, "done restart"); + + // check timer cancelled; may or may not have managed to take a photo + assertTrue(!mPreview.isOnTimer()); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + } + + /* Tests that selecting a new flash and focus option, then reopening the popup menu, still has the correct option highlighted. + */ + public void testPopup() { + Log.d(TAG, "testPopup"); + setToDefault(); + + switchToFlashValue("flash_off"); + switchToFlashValue("flash_on"); + + switchToFocusValue("focus_mode_macro"); + switchToFocusValue("focus_mode_auto"); + } + + /* Tests to do with video and popup menu. + */ + private void subTestVideoPopup(boolean on_timer) { + Log.d(TAG, "subTestVideoPopup"); + + assertTrue(!mPreview.isOnTimer()); + assertTrue(!mActivity.popupIsOpen()); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + + if( !mPreview.isVideo() ) { + View switchVideoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + clickView(switchVideoButton); + assertTrue(mPreview.isVideo()); + } + + if( !on_timer ) { + // open popup now + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + } + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + if( on_timer ) { + assertTrue(mPreview.isOnTimer()); + } + + try { + if( on_timer ) { + Thread.sleep(2000); + + // now open popup + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + + // check timer is cancelled + assertTrue( !mPreview.isOnTimer() ); + + // wait for timer (if it was still going) + Thread.sleep(4000); + + // now check we still aren't recording, and that popup is still open + assertTrue( mPreview.isVideo() ); + assertTrue( !mPreview.isTakingPhoto() ); + assertTrue( !mPreview.isOnTimer() ); + assertTrue( mActivity.popupIsOpen() ); + } + else { + Thread.sleep(1000); + + // now check we are recording video, and that popup is closed + assertTrue( mPreview.isVideo() ); + assertTrue( mPreview.isTakingPhoto() ); + assertTrue( !mActivity.popupIsOpen() ); + } + + if( !on_timer ) { + // (if on timer, the video will have stopped) + List supported_flash_values = mPreview.getSupportedFlashValues(); + if( supported_flash_values == null ) { + // button shouldn't show at all + assertTrue( popupButton.getVisibility() == View.GONE ); + } + else { + // now open popup again + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + subTestPopupButtonAvailability("TEST_FLASH", "flash_off", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_auto", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_on", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_torch", supported_flash_values); + subTestPopupButtonAvailability("TEST_FLASH", "flash_red_eye", supported_flash_values); + // only flash should be available + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_auto", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_locked", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_infinity", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_macro", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_fixed", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_edof", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_continuous_picture", null); + subTestPopupButtonAvailability("TEST_FOCUS", "focus_mode_continuous_video", null); + subTestPopupButtonAvailability("TEST_ISO", "auto", null); + subTestPopupButtonAvailability("TEST_ISO", "100", null); + subTestPopupButtonAvailability("TEST_ISO", "200", null); + subTestPopupButtonAvailability("TEST_ISO", "400", null); + subTestPopupButtonAvailability("TEST_ISO", "800", null); + subTestPopupButtonAvailability("TEST_ISO", "1600", null); + subTestPopupButtonAvailability("TEST_WHITE_BALANCE", false); + subTestPopupButtonAvailability("TEST_SCENE_MODE", false); + subTestPopupButtonAvailability("TEST_COLOR_EFFECT", false); + } + } + + Log.d(TAG, "now stop video"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking stop video"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue( !mPreview.isTakingPhoto() ); + assertTrue( !mActivity.popupIsOpen() ); + + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + + // now open popup again + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + subTestPopupButtonAvailability(); + } + + /* Tests that popup menu closes when we record video; then tests behaviour of popup. + */ + public void testVideoPopup() { + Log.d(TAG, "testVideoPopup"); + setToDefault(); + + subTestVideoPopup(false); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + subTestVideoPopup(false); + } + } + + /* Takes video on a timer, but checks that the popup menu stops video timer; then tests behaviour of popup. + */ + public void testVideoTimerPopup() { + Log.d(TAG, "testVideoTimerPopup"); + setToDefault(); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getTimerPreferenceKey(), "5"); + editor.putBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), false); + editor.apply(); + + subTestVideoPopup(true); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + subTestVideoPopup(true); + } + } + + /* Tests taking photos repeatedly with auto-repeat "burst" method. + */ + public void testTakePhotoBurst() { + Log.d(TAG, "testTakePhotoBurst"); + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getBurstModePreferenceKey(), "3"); + editor.apply(); + } + + // count initial files in folder + File folder = mActivity.getImageFolder(); + int n_files = folder.listFiles().length; + Log.d(TAG, "n_files at start: " + n_files); + + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + assertTrue(!mPreview.isOnTimer()); + + try { + // wait 6s, and test that we've taken the photos by then + Thread.sleep(6000); + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + Log.d(TAG, "count_cameraTakePicture: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==3); + int n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 3); + + // now test pausing and resuming + clickView(takePhotoButton); + pauseAndResume(); + // wait 5s, and test that we haven't taken any photos + Thread.sleep(5000); + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + assertTrue(mPreview.count_cameraTakePicture==3); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 3); + + // test with preview paused + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getPausePreviewPreferenceKey(), true); + editor.apply(); + } + clickView(takePhotoButton); + Thread.sleep(6000); + assertTrue(mPreview.count_cameraTakePicture==6); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 6); + assertTrue(!mPreview.isPreviewStarted()); // check preview paused + + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + this.getInstrumentation().waitForIdleSync(); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 6); + assertTrue(mPreview.isPreviewStarted()); // check preview restarted + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getPausePreviewPreferenceKey(), false); + editor.apply(); + } + + // now test burst interval + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getBurstModePreferenceKey(), "2"); + editor.putString(PreferenceKeys.getBurstIntervalPreferenceKey(), "3"); + editor.putBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), false); + editor.apply(); + } + clickView(takePhotoButton); + while( mPreview.isTakingPhoto() ) { + } + Log.d(TAG, "done taking 1st photo"); + this.getInstrumentation().waitForIdleSync(); + assertTrue(mPreview.count_cameraTakePicture==7); + mActivity.waitUntilImageQueueEmpty(); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 7); + // wait 2s, should still not have taken another photo + Thread.sleep(2000); + assertTrue(mPreview.count_cameraTakePicture==7); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 7); + // wait another 5s, should have taken another photo (need to allow time for the extra auto-focus) + Thread.sleep(5000); + assertTrue(mPreview.count_cameraTakePicture==8); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 8); + // wait 4s, should not have taken any more photos + Thread.sleep(4000); + assertTrue(mPreview.count_cameraTakePicture==8); + n_new_files = folder.listFiles().length - n_files; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == 8); + } + catch(InterruptedException e) { + e.printStackTrace(); + assertTrue(false); + } + } + + /* Tests that saving quality (i.e., resolution) settings can be done per-camera. Also checks that the supported picture sizes is as expected. + */ + public void testSaveQuality() { + Log.d(TAG, "testSaveQuality"); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + setToDefault(); + + List preview_sizes = mPreview.getSupportedPictureSizes(); + + // change back camera to the last size + CameraController.Size size = preview_sizes.get(preview_sizes.size()-1); + { + Log.d(TAG, "set size to " + size.width + " x " + size.height); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), size.width + " " + size.height); + editor.apply(); + } + + // need to resume activity for it to take effect (for camera to be reopened) + pauseAndResume(); + CameraController.Size new_size = mPreview.getCameraController().getPictureSize(); + Log.d(TAG, "size is now " + new_size.width + " x " + new_size.height); + assertTrue(size.equals(new_size)); + + // switch camera to front + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + + List front_preview_sizes = mPreview.getSupportedPictureSizes(); + + // change front camera to the last size + CameraController.Size front_size = front_preview_sizes.get(front_preview_sizes.size()-1); + { + Log.d(TAG, "set front_size to " + front_size.width + " x " + front_size.height); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), front_size.width + " " + front_size.height); + editor.apply(); + } + + // need to resume activity for it to take effect (for camera to be reopened) + pauseAndResume(); + // check still on front camera + Log.d(TAG, "camera id " + mPreview.getCameraId()); + assertTrue(mPreview.getCameraId() == new_cameraId); + CameraController.Size front_new_size = mPreview.getCameraController().getPictureSize(); + Log.d(TAG, "front size is now " + front_new_size.width + " x " + front_new_size.height); + assertTrue(front_size.equals(front_new_size)); + + // change front camera to the first size + front_size = front_preview_sizes.get(0); + { + Log.d(TAG, "set front_size to " + front_size.width + " x " + front_size.height); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), front_size.width + " " + front_size.height); + editor.apply(); + } + + // need to resume activity for it to take effect (for camera to be reopened) + pauseAndResume(); + front_new_size = mPreview.getCameraController().getPictureSize(); + Log.d(TAG, "front size is now " + front_new_size.width + " x " + front_new_size.height); + assertTrue(front_size.equals(front_new_size)); + + // switch camera to back + clickView(switchCameraButton); + new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId == new_cameraId); + + // now back camera size should still be what it was + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + String settings_size = settings.getString(PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId()), ""); + Log.d(TAG, "settings key is " + PreferenceKeys.getResolutionPreferenceKey(mPreview.getCameraId())); + Log.d(TAG, "settings size is " + settings_size); + } + new_size = mPreview.getCameraController().getPictureSize(); + Log.d(TAG, "size is now " + new_size.width + " x " + new_size.height); + assertTrue(size.equals(new_size)); + } + + private void testExif(String file, boolean expect_gps) throws IOException { + //final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; + //final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + ExifInterface exif = new ExifInterface(file); + assertTrue(exif.getAttribute(ExifInterface.TAG_ORIENTATION) != null); + assertTrue(exif.getAttribute(ExifInterface.TAG_MAKE) != null); + assertTrue(exif.getAttribute(ExifInterface.TAG_MODEL) != null); + if( expect_gps ) { + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF) != null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF) != null); + // can't read custom tags, even though we can write them?! + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION) != null); + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION_REF) != null); + } + else { + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF) == null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null); + assertTrue(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF) == null); + // can't read custom tags, even though we can write them?! + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION) == null); + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION_REF) == null); + } + } + + private void subTestLocationOn(boolean gps_direction) throws IOException { + Log.d(TAG, "subTestLocationOn"); + setToDefault(); + + assertTrue(!mActivity.getLocationSupplier().hasLocationListeners()); + Log.d(TAG, "turn on location"); + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getLocationPreferenceKey(), true); + if( gps_direction ) { + editor.putBoolean(PreferenceKeys.getGPSDirectionPreferenceKey(), true); + } + editor.apply(); + Log.d(TAG, "update settings after turning on location"); + updateForSettings(); + Log.d(TAG, "location should now be on"); + } + + assertTrue(mActivity.getLocationSupplier().hasLocationListeners()); + Log.d(TAG, "wait until received location"); + + long start_t = System.currentTimeMillis(); + while( !mActivity.getLocationSupplier().testHasReceivedLocation() ) { + this.getInstrumentation().waitForIdleSync(); + if( System.currentTimeMillis() - start_t > 20000 ) { + // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi + assertTrue(false); + } + } + Log.d(TAG, "have received location"); + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + mActivity.test_last_saved_image = null; + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mPreview.count_cameraTakePicture==1); + mActivity.waitUntilImageQueueEmpty(); + assertTrue(mActivity.test_last_saved_image != null); + testExif(mActivity.test_last_saved_image, true); + + // now test with auto-stabilise + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), true); + editor.apply(); + } + mActivity.test_last_saved_image = null; + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mPreview.count_cameraTakePicture==2); + mActivity.waitUntilImageQueueEmpty(); + assertTrue(mActivity.test_last_saved_image != null); + testExif(mActivity.test_last_saved_image, true); + + // switch to front camera + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + assertTrue(mActivity.getLocationSupplier().hasLocationListeners()); + // shouldn't need to wait for test_has_received_location to be true, as should remember from before switching camera + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + } + } + + /* Tests we save location data; also tests that we save other exif data. + * May fail on devices without mobile network, especially if we don't even have wifi. + */ + public void testLocationOn() throws IOException { + Log.d(TAG, "testLocationOn"); + subTestLocationOn(false); + } + + /* Tests we save location and gps direction. + * May fail on devices without mobile network, especially if we don't even have wifi. + */ + public void testLocationDirectionOn() throws IOException { + Log.d(TAG, "testLocationDirectionOn"); + subTestLocationOn(true); + } + + /* Tests we don't save location data; also tests that we save other exif data. + */ + private void subTestLocationOff(boolean gps_direction) throws IOException { + setToDefault(); + + if( gps_direction ) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getGPSDirectionPreferenceKey(), true); + editor.apply(); + updateForSettings(); + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(!mActivity.getLocationSupplier().hasLocationListeners()); + assertTrue(mActivity.getLocationSupplier().getLocation() == null); + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + mActivity.test_last_saved_image = null; + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mPreview.count_cameraTakePicture==1); + mActivity.waitUntilImageQueueEmpty(); + assertTrue(mActivity.test_last_saved_image != null); + testExif(mActivity.test_last_saved_image, false); + + // now test with auto-stabilise + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), true); + editor.apply(); + } + mActivity.test_last_saved_image = null; + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mPreview.count_cameraTakePicture==2); + mActivity.waitUntilImageQueueEmpty(); + assertTrue(mActivity.test_last_saved_image != null); + testExif(mActivity.test_last_saved_image, false); + + // switch to front camera + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() == null); + + clickView(switchCameraButton); + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() == null); + } + + // now switch location back on + Log.d(TAG, "now switch location back on"); + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getLocationPreferenceKey(), true); + editor.apply(); + restart(); // need to restart for this preference to take effect + } + + long start_t = System.currentTimeMillis(); + while( !mActivity.getLocationSupplier().testHasReceivedLocation() ) { + this.getInstrumentation().waitForIdleSync(); + if( System.currentTimeMillis() - start_t > 20000 ) { + // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi + assertTrue(false); + } + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + + // switch to front camera + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + // shouldn't need to wait for test_has_received_location to be true, as should remember from before switching camera + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + } + } + + /* Tests we don't save location data; also tests that we save other exif data. + * May fail on devices without mobile network, especially if we don't even have wifi. + */ + public void testLocationOff() throws IOException { + Log.d(TAG, "testLocationOff"); + subTestLocationOff(false); + } + + /* Tests we save gps direction. + * May fail on devices without mobile network, especially if we don't even have wifi. + */ + public void testDirectionOn() throws IOException { + Log.d(TAG, "testDirectionOn"); + subTestLocationOff(false); + } + + /* Tests we can stamp date/time and location to photo. + * May fail on devices without mobile network, especially if we don't even have wifi. + */ + public void testPhotoStamp() throws IOException { + Log.d(TAG, "testPhotoStamp"); + + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getStampPreferenceKey(), "preference_stamp_yes"); + editor.apply(); + updateForSettings(); + } + + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==1); + + // now again with location + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getLocationPreferenceKey(), true); + editor.apply(); + updateForSettings(); + } + + assertTrue( mActivity.getLocationSupplier().hasLocationListeners() ); + long start_t = System.currentTimeMillis(); + while( !mActivity.getLocationSupplier().testHasReceivedLocation() ) { + this.getInstrumentation().waitForIdleSync(); + if( System.currentTimeMillis() - start_t > 20000 ) { + // need to allow long time for testing devices without mobile network; will likely fail altogether if don't even have wifi + assertTrue(false); + } + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==2); + + // now again with custom text + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getTextStampPreferenceKey(), "Test stamp!£$"); + editor.apply(); + updateForSettings(); + } + + assertTrue( mActivity.getLocationSupplier().hasLocationListeners() ); + while( !mActivity.getLocationSupplier().testHasReceivedLocation() ) { + } + this.getInstrumentation().waitForIdleSync(); + assertTrue(mActivity.getLocationSupplier().getLocation() != null); + + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==3); + + // now test with auto-stabilise + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), true); + editor.apply(); + } + + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==4); + + } + + /* Tests we can stamp custom text to photo. + */ + public void testCustomTextStamp() throws IOException { + Log.d(TAG, "testCustomTextStamp"); + + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getTextStampPreferenceKey(), "Test stamp!£$"); + editor.apply(); + updateForSettings(); + } + + assertTrue(mPreview.count_cameraTakePicture==0); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==1); + + // now test with auto-stabilise + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), true); + editor.apply(); + } + + clickView(takePhotoButton); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "photo count: " + mPreview.count_cameraTakePicture); + assertTrue(mPreview.count_cameraTakePicture==2); + + } + + /* Tests zoom. + */ + public void testZoom() { + Log.d(TAG, "testZoom"); + setToDefault(); + + if( !mPreview.supportsZoom() ) { + Log.d(TAG, "zoom not supported"); + return; + } + + final ZoomControls zoomControls = (ZoomControls) mActivity.findViewById(net.sourceforge.opencamera.R.id.zoom); + assertTrue(zoomControls.getVisibility() == View.INVISIBLE); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getShowZoomControlsPreferenceKey(), true); + editor.apply(); + updateForSettings(); + + assertTrue(zoomControls.getVisibility() == View.VISIBLE); + final SeekBar zoomSeekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.zoom_seekbar); + assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); + int max_zoom = mPreview.getMaxZoom(); + assertTrue(zoomSeekBar.getMax() == max_zoom); + Log.d(TAG, "zoomSeekBar progress = " + zoomSeekBar.getProgress()); + Log.d(TAG, "actual zoom = " + mPreview.getCameraController().getZoom()); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + if( mPreview.supportsFocus() ) { + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // touch to auto-focus with focus area + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + + int zoom = mPreview.getCameraController().getZoom(); + + // use buttons to zoom + Log.d(TAG, "zoom in"); + mActivity.zoomIn(); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + zoom); + assertTrue(mPreview.getCameraController().getZoom() == zoom+1); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + if( mPreview.supportsFocus() ) { + // check that focus areas cleared + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // touch to auto-focus with focus area + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + + Log.d(TAG, "zoom out"); + mActivity.zoomOut(); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + zoom); + assertTrue(mPreview.getCameraController().getZoom() == zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + if( mPreview.supportsFocus() ) { + // check that focus areas cleared + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + + // touch to auto-focus with focus area + TouchUtils.clickView(MainActivityTest.this, mPreview.getView()); + assertTrue(mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() != null); + assertTrue(mPreview.getCameraController().getFocusAreas().size() == 1); + assertTrue(mPreview.getCameraController().getMeteringAreas() != null); + assertTrue(mPreview.getCameraController().getMeteringAreas().size() == 1); + } + + // now test multitouch zoom + mPreview.scaleZoom(2.0f); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + zoom); + assertTrue(mPreview.getCameraController().getZoom() > zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + mPreview.scaleZoom(0.5f); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + zoom); + assertTrue(mPreview.getCameraController().getZoom() == zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + // test to max/min + mPreview.scaleZoom(10000.0f); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to max_zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + mPreview.scaleZoom(1.0f/10000.0f); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zero"); + assertTrue(mPreview.getCameraController().getZoom() == 0); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + // use seekbar to zoom + Log.d(TAG, "zoom to max"); + Log.d(TAG, "progress was: " + zoomSeekBar.getProgress()); + zoomSeekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to max_zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + if( mPreview.supportsFocus() ) { + // check that focus areas cleared + assertTrue(!mPreview.hasFocusArea()); + assertTrue(mPreview.getCameraController().getFocusAreas() == null); + assertTrue(mPreview.getCameraController().getMeteringAreas() == null); + } + } + + public void testZoomIdle() { + Log.d(TAG, "testZoomIdle"); + setToDefault(); + + if( !mPreview.supportsZoom() ) { + Log.d(TAG, "zoom not supported"); + return; + } + + final SeekBar zoomSeekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.zoom_seekbar); + assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); + int max_zoom = mPreview.getMaxZoom(); + zoomSeekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + pauseAndResume(); + Log.d(TAG, "after pause and resume: compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + } + + public void testZoomSwitchCamera() { + Log.d(TAG, "testZoomSwitchCamera"); + setToDefault(); + + if( !mPreview.supportsZoom() ) { + Log.d(TAG, "zoom not supported"); + return; + } + else if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + + final SeekBar zoomSeekBar = (SeekBar) mActivity.findViewById(net.sourceforge.opencamera.R.id.zoom_seekbar); + assertTrue(zoomSeekBar.getVisibility() == View.VISIBLE); + int max_zoom = mPreview.getMaxZoom(); + zoomSeekBar.setProgress(0); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + + max_zoom = mPreview.getMaxZoom(); + Log.d(TAG, "after pause and resume: compare actual zoom " + mPreview.getCameraController().getZoom() + " to zoom " + max_zoom); + assertTrue(mPreview.getCameraController().getZoom() == max_zoom); + assertTrue(max_zoom-zoomSeekBar.getProgress() == mPreview.getCameraController().getZoom()); + } + + /** Switch to front camera, pause and resume, check still on the front camera. + */ + public void testSwitchCameraIdle() { + Log.d(TAG, "testSwitchCameraIdle"); + setToDefault(); + + if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + + int cameraId = mPreview.getCameraId(); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + int new_cameraId = mPreview.getCameraId(); + assertTrue(cameraId != new_cameraId); + + pauseAndResume(); + + int new2_cameraId = mPreview.getCameraId(); + assertTrue(new2_cameraId == new_cameraId); + + } + + /* Tests going to gallery. + */ + public void testGallery() { + Log.d(TAG, "testGallery"); + setToDefault(); + + View galleryButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.gallery); + clickView(galleryButton); + + } + + /* Tests going to settings. + */ + public void testSettings() { + Log.d(TAG, "testSettings"); + setToDefault(); + + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + clickView(settingsButton); + + } + + private void subTestCreateSaveFolder(boolean use_saf, String save_folder, boolean delete_folder) { + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + if( use_saf ) { + editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), true); + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), save_folder); + } + else { + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), save_folder); + } + editor.apply(); + updateForSettings(); + if( use_saf ) { + // need to call this directly, as we don't call mActivity.onActivityResult + mActivity.updateFolderHistorySAF(save_folder); + } + } + + SaveLocationHistory save_location_history = use_saf ? mActivity.getSaveLocationHistorySAF() : mActivity.getSaveLocationHistory(); + assertTrue(save_location_history.size() > 0); + assertTrue(save_location_history.contains(save_folder)); + assertTrue(save_location_history.get( save_location_history.size()-1 ).equals(save_folder)); + + File folder = mActivity.getImageFolder(); + if( folder.exists() && delete_folder ) { + assertTrue(folder.isDirectory()); + // delete folder - need to delete contents first + if( folder.isDirectory() ) { + String [] children = folder.list(); + for(String child : children) { + File file = new File(folder, child); + file.delete(); + MediaScannerConnection.scanFile(mActivity, new String[] { file.getAbsolutePath() }, null, null); + } + } + folder.delete(); + } + int n_old_files = 0; + if( folder.exists() ) { + n_old_files = folder.listFiles().length; + } + Log.d(TAG, "n_old_files: " + n_old_files); + + View takePhotoButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + + Log.d(TAG, "wait until finished taking photo"); + while( mPreview.isTakingPhoto() ) { + } + Log.d(TAG, "done taking photo"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + assertTrue(mPreview.count_cameraTakePicture==1); + + mActivity.waitUntilImageQueueEmpty(); + + assertTrue( folder.exists() ); + int n_new_files = folder.listFiles().length; + Log.d(TAG, "n_new_files: " + n_new_files); + assertTrue(n_new_files == n_old_files+1); + + // change back to default, so as to not be annoying + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + if( use_saf ) { + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), "content://com.android.externalstorage.documents/tree/primary%3ADCIM%2FOpenCamera"); + } + else { + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + } + editor.apply(); + } + } + + /** Tests taking a photo with a new save folder. + */ + public void testCreateSaveFolder1() { + Log.d(TAG, "testCreateSaveFolder1"); + subTestCreateSaveFolder(false, "OpenCameraTest", true); + } + + /** Tests taking a photo with a new save folder. + */ + public void testCreateSaveFolder2() { + Log.d(TAG, "testCreateSaveFolder2"); + subTestCreateSaveFolder(false, "OpenCameraTest/", true); + } + + /** Tests taking a photo with a new save folder. + */ + public void testCreateSaveFolder3() { + Log.d(TAG, "testCreateSaveFolder3"); + subTestCreateSaveFolder(false, "OpenCameraTest_a/OpenCameraTest_b", true); + } + + /** Tests taking a photo with a new save folder. + */ + @SuppressLint("SdCardPath") + public void testCreateSaveFolder4() { + Log.d(TAG, "testCreateSaveFolder4"); + subTestCreateSaveFolder(false, "/sdcard/Pictures/OpenCameraTest", true); + } + + /** Tests taking a photo with a new save folder. + */ + public void testCreateSaveFolderUnicode() { + Log.d(TAG, "testCreateSaveFolderUnicode"); + subTestCreateSaveFolder(false, "éúíóá!£$%^&()", true); + } + + /** Tests taking a photo with a new save folder. + */ + public void testCreateSaveFolderEmpty() { + Log.d(TAG, "testCreateSaveFolderEmpty"); + subTestCreateSaveFolder(false, "", false); + } + + /** Tests taking a photo with a new save folder. + * If this test fails, make sure we've manually selected that folder (as permission can't be given through the test framework). + */ + public void testCreateSaveFolderSAF() { + Log.d(TAG, "testCreateSaveFolderSAF"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + Log.d(TAG, "SAF requires Android Lollipop or better"); + return; + } + + subTestCreateSaveFolder(true, "content://com.android.externalstorage.documents/tree/primary%3ADCIM", true); + } + + /** Tests launching the folder chooser on a new folder. + */ + public void testFolderChooserNew() throws InterruptedException { + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCameraTest"); + editor.apply(); + updateForSettings(); + } + + File folder = mActivity.getImageFolder(); + if( folder.exists() ) { + assertTrue(folder.isDirectory()); + // delete folder - need to delete contents first + if( folder.isDirectory() ) { + String [] children = folder.list(); + for(String child : children) { + File file = new File(folder, child); + file.delete(); + MediaScannerConnection.scanFile(mActivity, new String[] { file.getAbsolutePath() }, null, null); + } + } + folder.delete(); + } + + FolderChooserDialog fragment = new FolderChooserDialog(); + fragment.show(mActivity.getFragmentManager(), "FOLDER_FRAGMENT"); + Thread.sleep(1000); // wait until folderchooser started up + Log.d(TAG, "started folderchooser"); + assertTrue(fragment.getCurrentFolder() != null); + assertTrue(fragment.getCurrentFolder().equals(folder)); + assertTrue(folder.exists()); + } + + /** Tests launching the folder chooser on a folder we don't have access to. + * (Shouldn't be possible to get into this state, but just in case.) + */ + public void testFolderChooserInvalid() throws InterruptedException { + setToDefault(); + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), "/OpenCameraTest"); + editor.apply(); + updateForSettings(); + } + + FolderChooserDialog fragment = new FolderChooserDialog(); + fragment.show(mActivity.getFragmentManager(), "FOLDER_FRAGMENT"); + Thread.sleep(1000); // wait until folderchooser started up + Log.d(TAG, "started folderchooser"); + assertTrue(fragment.getCurrentFolder() != null); + Log.d(TAG, "current folder: " + fragment.getCurrentFolder()); + assertTrue(fragment.getCurrentFolder().exists()); + } + + private void subTestSaveFolderHistory(final boolean use_saf) { + // clearFolderHistory has code that must be run on UI thread + mActivity.runOnUiThread(new Runnable() { + public void run() { + Log.d(TAG, "clearFolderHistory"); + if( use_saf ) + mActivity.clearFolderHistorySAF(); + else + mActivity.clearFolderHistory(); + } + }); + // need to wait for UI code to finish before leaving + this.getInstrumentation().waitForIdleSync(); + SaveLocationHistory save_location_history = use_saf ? mActivity.getSaveLocationHistorySAF() : mActivity.getSaveLocationHistory(); + Log.d(TAG, "save_location_history size: " + save_location_history.size()); + assertTrue(save_location_history.size() == 1); + String current_folder; + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + current_folder = use_saf ? settings.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), "") : settings.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + Log.d(TAG, "current_folder: " + current_folder); + Log.d(TAG, "save_location_history entry: " + save_location_history.get(0)); + assertTrue(save_location_history.get(0).equals(current_folder)); + } + + { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(use_saf ? PreferenceKeys.getSaveLocationSAFPreferenceKey() : PreferenceKeys.getSaveLocationPreferenceKey(), "new_folder_history_entry"); + editor.apply(); + updateForSettings(); + if( use_saf ) { + // need to call this directly, as we don't call mActivity.onActivityResult + mActivity.updateFolderHistorySAF("new_folder_history_entry"); + } + } + save_location_history = use_saf ? mActivity.getSaveLocationHistorySAF() : mActivity.getSaveLocationHistory(); + Log.d(TAG, "save_location_history size: " + save_location_history.size()); + for(int i=0;i scene_modes = mPreview.getSupportedSceneModes(); + if( scene_modes == null ) { + return; + } + Log.d(TAG, "scene mode: " + mPreview.getCameraController().getSceneMode()); + assertTrue( mPreview.getCameraController().getSceneMode() == null || mPreview.getCameraController().getSceneMode().equals(mPreview.getCameraController().getDefaultSceneMode()) ); + + String scene_mode = null; + // find a scene mode that isn't default + for(String this_scene_mode : scene_modes) { + if( !this_scene_mode.equals(mPreview.getCameraController().getDefaultSceneMode()) ) { + scene_mode = this_scene_mode; + break; + } + } + if( scene_mode == null ) { + return; + } + Log.d(TAG, "change to scene_mode: " + scene_mode); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getSceneModePreferenceKey(), scene_mode); + editor.apply(); + updateForSettings(); + + String new_scene_mode = mPreview.getCameraController().getSceneMode(); + Log.d(TAG, "scene_mode is now: " + new_scene_mode); + assertTrue( new_scene_mode.equals(scene_mode) ); + } + + public void testColorEffect() { + Log.d(TAG, "testColorEffect"); + + setToDefault(); + + List color_effects = mPreview.getSupportedColorEffects(); + if( color_effects == null ) { + return; + } + Log.d(TAG, "color effect: " + mPreview.getCameraController().getColorEffect()); + assertTrue( mPreview.getCameraController().getColorEffect() == null || mPreview.getCameraController().getColorEffect().equals(mPreview.getCameraController().getDefaultColorEffect()) ); + + String color_effect = null; + // find a color effect that isn't default + for(String this_color_effect : color_effects) { + if( !this_color_effect.equals(mPreview.getCameraController().getDefaultColorEffect()) ) { + color_effect = this_color_effect; + break; + } + } + if( color_effect == null ) { + return; + } + Log.d(TAG, "change to color_effect: " + color_effect); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getColorEffectPreferenceKey(), color_effect); + editor.apply(); + updateForSettings(); + + String new_color_effect = mPreview.getCameraController().getColorEffect(); + Log.d(TAG, "color_effect is now: " + new_color_effect); + assertTrue( new_color_effect.equals(color_effect) ); + } + + public void testWhiteBalance() { + Log.d(TAG, "testWhiteBalance"); + + setToDefault(); + + List white_balances = mPreview.getSupportedWhiteBalances(); + if( white_balances == null ) { + return; + } + Log.d(TAG, "white balance: " + mPreview.getCameraController().getWhiteBalance()); + assertTrue( mPreview.getCameraController().getWhiteBalance() == null || mPreview.getCameraController().getWhiteBalance().equals(mPreview.getCameraController().getDefaultWhiteBalance()) ); + + String white_balance = null; + // find a white balance that isn't default + for(String this_white_balances : white_balances) { + if( !this_white_balances.equals(mPreview.getCameraController().getDefaultWhiteBalance()) ) { + white_balance = this_white_balances; + break; + } + } + if( white_balance == null ) { + return; + } + Log.d(TAG, "change to white_balance: " + white_balance); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getWhiteBalancePreferenceKey(), white_balance); + editor.apply(); + updateForSettings(); + + String new_white_balance = mPreview.getCameraController().getWhiteBalance(); + Log.d(TAG, "white_balance is now: " + new_white_balance); + assertTrue( new_white_balance.equals(white_balance) ); + } + + public void testImageQuality() { + Log.d(TAG, "testImageQuality"); + + setToDefault(); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getQualityPreferenceKey(), "100"); + editor.apply(); + updateForSettings(); + + int quality = mPreview.getCameraController().getJpegQuality(); + Log.d(TAG, "quality is: " + quality); + assertTrue( quality == 100 ); + } + + /** Tests that changing resolutions doesn't close the popup. + */ + public void testSwitchResolution() throws InterruptedException { + Log.d(TAG, "testSwitchResolution"); + + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + CameraController.Size old_picture_size = mPreview.getCameraController().getPictureSize(); + + // open popup + assertFalse( mActivity.popupIsOpen() ); + clickView(popupButton); + while( !mActivity.popupIsOpen() ) { + } + + TextView photoResolutionButton = (TextView)mActivity.getPopupButton("PHOTO_RESOLUTIONS"); + assertTrue(photoResolutionButton != null); + String exp_size_string = old_picture_size.width + " x " + old_picture_size.height + " " + Preview.getMPString(old_picture_size.width, old_picture_size.height); + Log.d(TAG, "size string: " + photoResolutionButton.getText()); + assertTrue( photoResolutionButton.getText().equals(exp_size_string) ); + + // change photo resolution + View photoResolutionNextButton = mActivity.getPopupButton("PHOTO_RESOLUTIONS_NEXT"); + assertTrue(photoResolutionNextButton != null); + this.getInstrumentation().waitForIdleSync(); + clickView(photoResolutionNextButton); + + // check + Thread.sleep(2000); + CameraController.Size new_picture_size = mPreview.getCameraController().getPictureSize(); + Log.d(TAG, "old picture size: " + old_picture_size.width + " x " + old_picture_size.height); + Log.d(TAG, "old new_picture_size size: " + new_picture_size.width + " x " + new_picture_size.height); + assertTrue( !new_picture_size.equals(old_picture_size) ); + assertTrue( mActivity.popupIsOpen() ); + + exp_size_string = new_picture_size.width + " x " + new_picture_size.height + " " + Preview.getMPString(new_picture_size.width, new_picture_size.height); + Log.d(TAG, "size string: " + photoResolutionButton.getText()); + assertTrue( photoResolutionButton.getText().equals(exp_size_string) ); + + TextView videoResolutionButton = (TextView)mActivity.getPopupButton("VIDEO_RESOLUTIONS"); + assertTrue(videoResolutionButton != null); + CharSequence oldVideoResolutionString = videoResolutionButton.getText(); + + // change video resolution + View videoResolutionNextButton = mActivity.getPopupButton("VIDEO_RESOLUTIONS_NEXT"); + assertTrue(videoResolutionNextButton != null); + clickView(videoResolutionNextButton); + + // check + Thread.sleep(500); + assertTrue( mActivity.popupIsOpen() ); + assertTrue( !videoResolutionButton.getText().equals(oldVideoResolutionString) ); + + } + + /* Test for failing to open camera. + */ + public void testFailOpenCamera() throws InterruptedException { + Log.d(TAG, "testFailOpenCamera"); + + setToDefault(); + + assertTrue(mPreview.getCameraControllerManager() != null); + assertTrue(mPreview.getCameraController() != null); + mPreview.test_fail_open_camera = true; + + // can't test on startup, as camera is created when we create activity, so instead test by switching camera + if( mPreview.getCameraControllerManager().getNumberOfCameras() > 1 ) { + Log.d(TAG, "switch camera"); + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + assertTrue(mPreview.getCameraControllerManager() != null); + assertTrue(mPreview.getCameraController() == null); + this.getInstrumentation().waitForIdleSync(); + + assertFalse( mActivity.popupIsOpen() ); + View popupButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.popup); + Log.d(TAG, "about to click popup"); + clickView(popupButton); + Log.d(TAG, "done clicking popup"); + Thread.sleep(500); + // if camera isn't opened, popup shouldn't open + assertFalse( mActivity.popupIsOpen() ); + + View settingsButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.settings); + Log.d(TAG, "about to click settings"); + clickView(settingsButton); + Log.d(TAG, "done clicking settings"); + this.getInstrumentation().waitForIdleSync(); + Log.d(TAG, "after idle sync"); + } + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getVolumeKeysPreferenceKey(), "volume_exposure"); + editor.apply(); + this.getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_VOLUME_UP); + } + + public void testTakePhotoDRO() throws InterruptedException { + Log.d(TAG, "testTakePhotoDRO"); + if( !mActivity.supportsDRO() ) { + return; + } + + setToDefault(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 90 ); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_dro"); + editor.apply(); + updateForSettings(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 100 ); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 100 ); + + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std"); + editor.apply(); + updateForSettings(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 90 ); + } + + public void testTakePhotoDROPhotoStamp() throws InterruptedException { + Log.d(TAG, "testTakePhotoDROPhotoStamp"); + if( !mActivity.supportsDRO() ) { + return; + } + + setToDefault(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 90 ); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_dro"); + editor.putString(PreferenceKeys.getStampPreferenceKey(), "preference_stamp_yes"); + editor.apply(); + updateForSettings(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 100 ); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 100 ); + + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std"); + editor.apply(); + updateForSettings(); + + assertTrue( mActivity.getApplicationInterface().getImageQualityPref() == 90 ); + } + + public void testTakePhotoHDR() throws InterruptedException { + Log.d(TAG, "testTakePhotoHDR"); + if( !mActivity.supportsHDR() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_hdr"); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + public void testTakePhotoHDRSaveExpo() throws InterruptedException { + Log.d(TAG, "testTakePhotoHDRSaveExpo"); + if( !mActivity.supportsHDR() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_hdr"); + editor.putBoolean(PreferenceKeys.getHDRSaveExpoPreferenceKey(), true); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + public void testTakePhotoHDRFrontCamera() throws InterruptedException { + Log.d(TAG, "testTakePhotoHDRFrontCamera"); + if( !mActivity.supportsHDR() ) { + return; + } + if( mPreview.getCameraControllerManager().getNumberOfCameras() <= 1 ) { + return; + } + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_hdr"); + editor.apply(); + updateForSettings(); + + int cameraId = mPreview.getCameraId(); + + View switchCameraButton = mActivity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + clickView(switchCameraButton); + + int new_cameraId = mPreview.getCameraId(); + + Log.d(TAG, "cameraId: " + cameraId); + Log.d(TAG, "new_cameraId: " + new_cameraId); + + assertTrue(cameraId != new_cameraId); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + public void testTakePhotoHDRAutoStabilise() throws InterruptedException { + Log.d(TAG, "testTakePhotoHDRAutoStabilise"); + if( !mActivity.supportsHDR() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_hdr"); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), true); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + public void testTakePhotoHDRPhotoStamp() throws InterruptedException { + Log.d(TAG, "testTakePhotoHDRPhotoStamp"); + if( !mActivity.supportsHDR() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_hdr"); + editor.putString(PreferenceKeys.getStampPreferenceKey(), "preference_stamp_yes"); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + /** Tests expo bracketing with default values. + */ + public void testTakePhotoExpo() throws InterruptedException { + Log.d(TAG, "testTakePhotoExpo"); + if( !mActivity.supportsExpoBracketing() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_expo_bracketing"); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + /** Tests expo bracketing with 5 images, 1 stop. + * Note this test [usually] fails on OnePlus 3T as onImageAvailable is only called 4 times, we never receive the 5th image. + */ + public void testTakePhotoExpo5() throws InterruptedException { + Log.d(TAG, "testTakePhotoExpo5"); + if( !mActivity.supportsExpoBracketing() ) { + return; + } + + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_expo_bracketing"); + editor.putString(PreferenceKeys.getExpoBracketingNImagesPreferenceKey(), "5"); + editor.putString(PreferenceKeys.getExpoBracketingStopsPreferenceKey(), "1"); + editor.apply(); + updateForSettings(); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + Log.d(TAG, "test_capture_results: " + mPreview.getCameraController().test_capture_results); + assertTrue(mPreview.getCameraController().test_capture_results == 1); + } + + /*private Bitmap getBitmapFromAssets(String filename) throws IOException { + Log.d(TAG, "getBitmapFromAssets: " + filename); + AssetManager assetManager = getInstrumentation().getContext().getResources().getAssets(); + InputStream is = assetManager.open(filename); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + Bitmap bitmap = BitmapFactory.decodeStream(is, null, options); + is.close(); + Log.d(TAG, " done: " + bitmap); + return bitmap; + }*/ + + private Bitmap getBitmapFromFile(String filename) throws FileNotFoundException { + Log.d(TAG, "getBitmapFromFile: " + filename); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + Bitmap bitmap = BitmapFactory.decodeFile(filename, options); + if( bitmap == null ) + throw new FileNotFoundException(); + Log.d(TAG, " done: " + bitmap); + return bitmap; + } + + /* Tests restarting a large number of times - can be useful for testing for memory/resource leaks. + */ + public void testRestart() { + Log.d(TAG, "testRestart"); + setToDefault(); + + final int n_restarts = 150; + for(int i=0;i inputs, String output_name, boolean test_dro) throws IOException, InterruptedException { + Log.d(TAG, "subTestHDR"); + + Thread.sleep(1000); // wait for camera to open + + Bitmap dro_bitmap_in = null; + if( test_dro ) { + // save copy of input bitmap to also test DRO (since the HDR routine will free the inputs) + //dro_bitmap_in = inputs.get(0); + dro_bitmap_in = inputs.get(1); + dro_bitmap_in = dro_bitmap_in.copy(dro_bitmap_in.getConfig(), true); + } + + long time_s = System.currentTimeMillis(); + mActivity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true); + Log.d(TAG, "HDR time: " + (System.currentTimeMillis() - time_s)); + + File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/" + output_name); + OutputStream outputStream = new FileOutputStream(file); + inputs.get(0).compress(Bitmap.CompressFormat.JPEG, 90, outputStream); + outputStream.close(); + mActivity.getStorageUtils().broadcastFile(file, true, false, true); + inputs.get(0).recycle(); + inputs.clear(); + + if( test_dro ) { + inputs.add(dro_bitmap_in); + time_s = System.currentTimeMillis(); + mActivity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true); + Log.d(TAG, "DRO time: " + (System.currentTimeMillis() - time_s)); + + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/dro" + output_name); + outputStream = new FileOutputStream(file); + inputs.get(0).compress(Bitmap.CompressFormat.JPEG, 90, outputStream); + outputStream.close(); + mActivity.getStorageUtils().broadcastFile(file, true, false, true); + inputs.get(0).recycle(); + inputs.clear(); + } + Thread.sleep(500); + } + + /** Checks that the HDR offsets used for auto-alignment are as expected. + */ + private void checkHDROffsets(int [] exp_offsets_x, int [] exp_offsets_y) { + int [] offsets_x = mActivity.getApplicationInterface().getHDRProcessor().offsets_x; + int [] offsets_y = mActivity.getApplicationInterface().getHDRProcessor().offsets_y; + for(int i=0;i<3;i++) { + Log.d(TAG, "offsets " + i + " ( " + offsets_x[i] + " , " + offsets_y[i] + " ), expected ( " + exp_offsets_x[i] + " , " + exp_offsets_y[i] + ")"); + assertTrue( offsets_x[i] == exp_offsets_x[i] ); + assertTrue( offsets_y[i] == exp_offsets_y[i] ); + } + } + + final private String hdr_images_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/testOpenCamera/testdata/hdrsamples/"; + + /** Tests HDR algorithm on test samples "saintpaul". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR1() throws IOException, InterruptedException { + Log.d(TAG, "testHDR1"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input2.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input3.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input4.jpg") ); + + subTestHDR(inputs, "testHDR1_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "saintpaul". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR2() throws IOException, InterruptedException { + Log.d(TAG, "testHDR2"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input2.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input3.jpg") ); + + subTestHDR(inputs, "testHDR2_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 2}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR3". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR3() throws IOException, InterruptedException { + Log.d(TAG, "testHDR3"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input2.jpg") ); + + subTestHDR(inputs, "testHDR3_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {1, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR4". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR4() throws IOException, InterruptedException { + Log.d(TAG, "testHDR4"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input2.jpg") ); + + subTestHDR(inputs, "testHDR4_output.jpg", true); + + int [] exp_offsets_x = {-2, 0, 2}; + int [] exp_offsets_y = {-1, 0, 1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR5". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR5() throws IOException, InterruptedException { + Log.d(TAG, "testHDR5"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input2.jpg") ); + + subTestHDR(inputs, "testHDR5_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {-1, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR6". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR6() throws IOException, InterruptedException { + Log.d(TAG, "testHDR6"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input2.jpg") ); + + subTestHDR(inputs, "testHDR6_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {1, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR7". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR7() throws IOException, InterruptedException { + Log.d(TAG, "testHDR7"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input2.jpg") ); + + subTestHDR(inputs, "testHDR7_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR8". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR8() throws IOException, InterruptedException { + Log.d(TAG, "testHDR8"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input2.jpg") ); + + subTestHDR(inputs, "testHDR8_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR9". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR9() throws IOException, InterruptedException { + Log.d(TAG, "testHDR9"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input2.jpg") ); + + subTestHDR(inputs, "testHDR9_output.jpg", false); + + int [] exp_offsets_x = {-1, 0, 1}; + int [] exp_offsets_y = {0, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR10". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR10() throws IOException, InterruptedException { + Log.d(TAG, "testHDR10"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input2.jpg") ); + + subTestHDR(inputs, "testHDR10_output.jpg", false); + + int [] exp_offsets_x = {2, 0, 0}; + int [] exp_offsets_y = {5, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR11". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR11() throws IOException, InterruptedException { + Log.d(TAG, "testHDR11"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input2.jpg") ); + + subTestHDR(inputs, "testHDR11_output.jpg", true); + + int [] exp_offsets_x = {-2, 0, 1}; + int [] exp_offsets_y = {1, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR12". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR12() throws IOException, InterruptedException { + Log.d(TAG, "testHDR12"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input2.jpg") ); + + subTestHDR(inputs, "testHDR12_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 7}; + int [] exp_offsets_y = {0, 0, 8}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR13". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR13() throws IOException, InterruptedException { + Log.d(TAG, "testHDR13"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input2.jpg") ); + + subTestHDR(inputs, "testHDR13_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 2}; + int [] exp_offsets_y = {0, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR14". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR14() throws IOException, InterruptedException { + Log.d(TAG, "testHDR14"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input2.jpg") ); + + subTestHDR(inputs, "testHDR14_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 1}; + int [] exp_offsets_y = {0, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR15". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR15() throws IOException, InterruptedException { + Log.d(TAG, "testHDR15"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input2.jpg") ); + + subTestHDR(inputs, "testHDR15_output.jpg", false); + + int [] exp_offsets_x = {1, 0, -1}; + int [] exp_offsets_y = {2, 0, -3}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR16". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR16() throws IOException, InterruptedException { + Log.d(TAG, "testHDR16"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input2.jpg") ); + + subTestHDR(inputs, "testHDR16_output.jpg", false); + + int [] exp_offsets_x = {-1, 0, 2}; + int [] exp_offsets_y = {1, 0, -6}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR17". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR17() throws IOException, InterruptedException { + Log.d(TAG, "testHDR17"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input2.jpg") ); + + subTestHDR(inputs, "testHDR17_output.jpg", true); + + int [] exp_offsets_x = {0, 0, -3}; + int [] exp_offsets_y = {0, 0, -4}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR18". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR18() throws IOException, InterruptedException { + Log.d(TAG, "testHDR18"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input2.jpg") ); + + subTestHDR(inputs, "testHDR18_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR19". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR19() throws IOException, InterruptedException { + Log.d(TAG, "testHDR19"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input2.jpg") ); + + subTestHDR(inputs, "testHDR19_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR20". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR20() throws IOException, InterruptedException { + Log.d(TAG, "testHDR20"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input2.jpg") ); + + subTestHDR(inputs, "testHDR20_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {-1, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR21". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR21() throws IOException, InterruptedException { + Log.d(TAG, "testHDR21"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input2.jpg") ); + + subTestHDR(inputs, "testHDR21_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR22". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR22() throws IOException, InterruptedException { + Log.d(TAG, "testHDR22"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input2.jpg") ); + + subTestHDR(inputs, "testHDR22_output.jpg", true); + + int [] exp_offsets_x = {1, 0, -5}; + int [] exp_offsets_y = {1, 0, -6}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR23". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR23() throws IOException, InterruptedException { + Log.d(TAG, "testHDR23"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0066.png") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") ); + + subTestHDR(inputs, "testHDR23_output.jpg", false); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR24". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR24() throws IOException, InterruptedException { + Log.d(TAG, "testHDR24"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input2.jpg") ); + + subTestHDR(inputs, "testHDR24_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 1}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR25". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR25() throws IOException, InterruptedException { + Log.d(TAG, "testHDR25"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input2.jpg") ); + + subTestHDR(inputs, "testHDR25_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 0}; + int [] exp_offsets_y = {1, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR26". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR26() throws IOException, InterruptedException { + Log.d(TAG, "testHDR26"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input2.jpg") ); + + subTestHDR(inputs, "testHDR26_output.jpg", true); + + int [] exp_offsets_x = {-1, 0, 1}; + int [] exp_offsets_y = {1, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR27". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR27() throws IOException, InterruptedException { + Log.d(TAG, "testHDR27"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input2.jpg") ); + + subTestHDR(inputs, "testHDR27_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 2}; + int [] exp_offsets_y = {0, 0, 0}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR28". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR28() throws IOException, InterruptedException { + Log.d(TAG, "testHDR28"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input2.jpg") ); + + subTestHDR(inputs, "testHDR28_output.jpg", true); + + int [] exp_offsets_x = {0, 0, 2}; + int [] exp_offsets_y = {0, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR29". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR29() throws IOException, InterruptedException { + Log.d(TAG, "testHDR29"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input2.jpg") ); + + subTestHDR(inputs, "testHDR29_output.jpg", false); + + int [] exp_offsets_x = {-1, 0, 3}; + int [] exp_offsets_y = {0, 0, -1}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR30". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR30() throws IOException, InterruptedException { + Log.d(TAG, "testHDR30"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input2.jpg") ); + + subTestHDR(inputs, "testHDR30_output.jpg", false); + + // offsets for full image + //int [] exp_offsets_x = {-6, 0, -1}; + //int [] exp_offsets_y = {23, 0, -13}; + // offsets using centre quarter image + int [] exp_offsets_x = {-5, 0, 0}; + int [] exp_offsets_y = {22, 0, -13}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR31". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR31() throws IOException, InterruptedException { + Log.d(TAG, "testHDR31"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input2.jpg") ); + + subTestHDR(inputs, "testHDR31_output.jpg", false); + + // offsets for full image + //int [] exp_offsets_x = {0, 0, 4}; + //int [] exp_offsets_y = {21, 0, -11}; + // offsets using centre quarter image + int [] exp_offsets_x = {0, 0, 3}; + int [] exp_offsets_y = {21, 0, -11}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR32". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR32() throws IOException, InterruptedException { + Log.d(TAG, "testHDR32"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input2.jpg") ); + + subTestHDR(inputs, "testHDR32_output.jpg", true); + + int [] exp_offsets_x = {1, 0, 0}; + int [] exp_offsets_y = {13, 0, -10}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR33". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR33() throws IOException, InterruptedException { + Log.d(TAG, "testHDR33"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input2.jpg") ); + + subTestHDR(inputs, "testHDR33_output.jpg", true); + + int [] exp_offsets_x = {13, 0, -10}; + int [] exp_offsets_y = {24, 0, -12}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR34". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR34() throws IOException, InterruptedException { + Log.d(TAG, "testHDR34"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input2.jpg") ); + + subTestHDR(inputs, "testHDR34_output.jpg", true); + + int [] exp_offsets_x = {5, 0, -8}; + int [] exp_offsets_y = {0, 0, -2}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDR35". + * @throws IOException + * @throws InterruptedException + */ + public void testHDR35() throws IOException, InterruptedException { + Log.d(TAG, "testHDR35"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input2.jpg") ); + + subTestHDR(inputs, "testHDR35_output.jpg", true); + + int [] exp_offsets_x = {-10, 0, 3}; + int [] exp_offsets_y = {7, 0, -3}; + checkHDROffsets(exp_offsets_x, exp_offsets_y); + } + + /** Tests HDR algorithm on test samples "testHDRtemp". + * Used for one-off testing, or to recreate HDR images from the base exposures to test an updated alorithm. + * The test images should be copied to the test device into DCIM/testOpenCamera/testdata/hdrsamples/testHDRtemp/ . + * @throws IOException + * @throws InterruptedException + */ + public void testHDRtemp() throws IOException, InterruptedException { + Log.d(TAG, "testHDRtemp"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input2.jpg") ); + + subTestHDR(inputs, "testHDRtemp_output.jpg", true); + } +} diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/MainTests.java b/src/androidTest/java/net/sourceforge/opencamera/test/MainTests.java new file mode 100644 index 00000000..aad4e1fb --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/MainTests.java @@ -0,0 +1,62 @@ +package net.sourceforge.opencamera.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class MainTests { + // Tests that don't fit into another of the Test suites + public static Test suite() { + /*return new TestSuiteBuilder(AllTests.class) + .includeAllPackagesUnderHere() + .build();*/ + TestSuite suite = new TestSuite(MainTests.class.getName()); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testStartCameraPreviewCount")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveVideoMode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveFocusMode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveFlashTorchQuit")); + //suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveFlashTorchSwitchCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFlashStartup")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFlashStartup2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testPreviewSize")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testPreviewSizeWYSIWYG")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testAutoFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testAutoFocusCorners")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testPopup")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSwitchResolution")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFaceDetection")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusFlashAvailability")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSwitchVideo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusSwitchVideoSwitchCameras")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusRemainMacroSwitchCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusRemainMacroSwitchPhoto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusSaveMacroSwitchPhoto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFocusSwitchVideoResetContinuous")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPictureFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPictureRepeatTouch")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPictureSwitchAuto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousVideoFocusForPhoto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testStartupAutoFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testExposureLockNotSaved")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveQuality")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testZoom")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testZoomIdle")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testZoomSwitchCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSwitchCameraIdle")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testGallery")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSettings")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFolderChooserNew")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFolderChooserInvalid")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveFolderHistory")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSaveFolderHistorySAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testPreviewRotation")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testSceneMode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testColorEffect")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testWhiteBalance")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testImageQuality")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testFailOpenCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testAudioControlIcon")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testOnError")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testGPSString")); + return suite; + } +} diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/PhotoCamera2Tests.java b/src/androidTest/java/net/sourceforge/opencamera/test/PhotoCamera2Tests.java new file mode 100644 index 00000000..8d488183 --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/PhotoCamera2Tests.java @@ -0,0 +1,28 @@ +package net.sourceforge.opencamera.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class PhotoCamera2Tests { + // Tests related to taking photos that require Camera2 - only need to run this suite with Camera2 + public static Test suite() { + TestSuite suite = new TestSuite(MainTests.class.getName()); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoManualFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoManualISOExposure")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoRaw")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoRawMulti")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoRawWaitCaptureResult")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedTrashRaw")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedTrashRaw2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoHDR")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoHDRSaveExpo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoHDRFrontCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoHDRAutoStabilise")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoHDRPhotoStamp")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoExpo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoExpo5")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashAutoFakeMode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashOnFakeMode")); + return suite; + } +} diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/PhotoTests.java b/src/androidTest/java/net/sourceforge/opencamera/test/PhotoTests.java new file mode 100644 index 00000000..529d7b94 --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/PhotoTests.java @@ -0,0 +1,67 @@ +package net.sourceforge.opencamera.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class PhotoTests { + // Tests related to taking photos; note that tests to do with photo mode that don't take photos are still part of MainTests + public static Test suite() { + TestSuite suite = new TestSuite(MainTests.class.getName()); + // put these tests first as they require various permissions be allowed, that can only be set by user action + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoSAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLocationOn")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLocationDirectionOn")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLocationOff")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testDirectionOn")); + // other tests: + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhoto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoContinuous")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoContinuousNoTouch")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAutoStabilise")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashAuto")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashOn")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashTorch")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAudioButton")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoNoAutofocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoNoThumbnail")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFlashBug")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFrontCamera")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFrontCameraScreenFlash")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoLockedFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoExposureCompensation")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoLockedLandscape")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoLockedPortrait")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPaused")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedAudioButton")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedSAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedTrash")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedTrashSAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoPreviewPausedTrash2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoQuickFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoRepeatFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoRepeatFocusLocked")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAfterFocus")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoSingleTap")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoDoubleTap")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAlt")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAutoLevel")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoAutoLevelAngles")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTimerBackground")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTimerSettings")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTimerPopup")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoBurst")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPicture1")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPicture2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testContinuousPictureFocusBurst")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testPhotoStamp")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoDRO")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoDROPhotoStamp")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolder1")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolder2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolder3")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolder4")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolderUnicode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testCreateSaveFolderEmpty")); + return suite; + } +} diff --git a/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java b/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java new file mode 100644 index 00000000..c70c92be --- /dev/null +++ b/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java @@ -0,0 +1,44 @@ +package net.sourceforge.opencamera.test; + +import junit.framework.Test; +import junit.framework.TestSuite; +public class VideoTests { + // Tests related to video recording; note that tests to do with video mode that don't record are still part of MainTests + public static Test suite() { + TestSuite suite = new TestSuite(MainTests.class.getName()); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoAudioControl")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSubtitles")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testImmersiveMode")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testImmersiveModeEverything")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoStabilization")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoExposureLock")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFocusArea")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoQuick")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoQuickSAF")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxDuration")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxDurationRestart")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxDurationRestartInterrupt")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSettings")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMacro")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoPause")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoPauseStop")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFlashVideo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoTimerInterrupt")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoPopup")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoTimerPopup")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoAvailableMemory")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoAvailableMemory2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxFileSize1")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxFileSize2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoMaxFileSize3")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoForceFailure")); + // put tests which change bitrate, fps or test 4K at end + // update: now deprecating these tests, as setting these settings can be dodgy on some devices + /*suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoBitrate")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFPS")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideo4K"));*/ + return suite; + } +} diff --git a/src/androidTest/res/drawable-hdpi/ic_launcher.png b/src/androidTest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..96a442e5 Binary files /dev/null and b/src/androidTest/res/drawable-hdpi/ic_launcher.png differ diff --git a/src/androidTest/res/drawable-ldpi/ic_launcher.png b/src/androidTest/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..99238729 Binary files /dev/null and b/src/androidTest/res/drawable-ldpi/ic_launcher.png differ diff --git a/src/androidTest/res/drawable-mdpi/ic_launcher.png b/src/androidTest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..359047df Binary files /dev/null and b/src/androidTest/res/drawable-mdpi/ic_launcher.png differ diff --git a/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/src/androidTest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..71c6d760 Binary files /dev/null and b/src/androidTest/res/drawable-xhdpi/ic_launcher.png differ diff --git a/src/androidTest/res/values/strings.xml b/src/androidTest/res/values/strings.xml new file mode 100644 index 00000000..68353390 --- /dev/null +++ b/src/androidTest/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + OpenCamera.testTest + + diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d0de946d --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/net/sourceforge/opencamera/AudioListener.java b/src/main/java/net/sourceforge/opencamera/AudioListener.java new file mode 100644 index 00000000..b47e8ada --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/AudioListener.java @@ -0,0 +1,187 @@ +package net.sourceforge.opencamera; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.util.Log; + +/** Sets up a listener to listen for noise level. + */ +class AudioListener { + private static final String TAG = "AudioListener"; + private volatile boolean is_running = true; // should be volatile, as used to communicate between threads + private int buffer_size = -1; + private AudioRecord ar; // modification to ar should always be synchronized (on AudioListener.this), as the ar can be released in the AudioListener's own thread + private Thread thread; + + public interface AudioListenerCallback { + void onAudio(int level); + } + + /** Create a new AudioListener. The caller should call the start() method to start listening. + */ + AudioListener(final AudioListenerCallback cb) { + if( MyDebug.LOG ) + Log.d(TAG, "new AudioListener"); + final int sample_rate = 8000; + int channel_config = AudioFormat.CHANNEL_IN_MONO; + int audio_format = AudioFormat.ENCODING_PCM_16BIT; + try { + buffer_size = AudioRecord.getMinBufferSize(sample_rate, channel_config, audio_format); + //buffer_size = -1; // test + if( MyDebug.LOG ) + Log.d(TAG, "buffer_size: " + buffer_size); + if( buffer_size <= 0 ) { + if( MyDebug.LOG ) { + if( buffer_size == AudioRecord.ERROR ) + Log.e(TAG, "getMinBufferSize returned ERROR"); + else if( buffer_size == AudioRecord.ERROR_BAD_VALUE ) + Log.e(TAG, "getMinBufferSize returned ERROR_BAD_VALUE"); + } + return; + } + + synchronized(AudioListener.this) { + ar = new AudioRecord(MediaRecorder.AudioSource.MIC, sample_rate, channel_config, audio_format, buffer_size); + AudioListener.this.notifyAll(); // probably not needed currently as no thread should be waiting for creation, but just for consistency + } + } + catch(Exception e) { + e.printStackTrace(); + Log.e(TAG, "failed to create audiorecord"); + return; + } + + // check initialised + synchronized(AudioListener.this) { + if( ar.getState() == AudioRecord.STATE_INITIALIZED ) { + if( MyDebug.LOG ) + Log.d(TAG, "audiorecord is initialised"); + } + else { + Log.e(TAG, "audiorecord failed to initialise"); + ar.release(); + ar = null; + AudioListener.this.notifyAll(); // again probably not needed, but just in case + return; + } + } + + final short[] buffer = new short[buffer_size]; + ar.startRecording(); + + this.thread = new Thread() { + @Override + public void run() { + /*int sample_delay = (1000 * buffer_size) / sample_rate; + if( MyDebug.LOG ) + Log.e(TAG, "sample_delay: " + sample_delay);*/ + + while( is_running ) { + /*try{ + Thread.sleep(sample_delay); + } + catch(InterruptedException e) { + e.printStackTrace(); + }*/ + try { + int n_read = ar.read(buffer, 0, buffer_size); + if( n_read > 0 ) { + int average_noise = 0; + int max_noise = 0; + for(int i=0;i zoom_ratios; + public boolean supports_face_detection; + public List picture_sizes; + public List video_sizes; + public List video_sizes_high_speed; + public List preview_sizes; + public List supported_flash_values; + public List supported_focus_values; + public int max_num_focus_areas; + public float minimum_focus_distance; + public boolean is_exposure_lock_supported; + public boolean is_video_stabilization_supported; + public boolean supports_iso_range; + public int min_iso; + public int max_iso; + public boolean supports_exposure_time; + public long min_exposure_time; + public long max_exposure_time; + public int min_exposure; + public int max_exposure; + public float exposure_step; + public boolean can_disable_shutter_sound; + public boolean supports_expo_bracketing; + public boolean supports_raw; + public float view_angle_x; // horizontal angle of view in degrees (when unzoomed) + public float view_angle_y; // view angle of view in degrees (when unzoomed) + } + + public static class Size { + public final int width; + public final int height; + + public Size(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public boolean equals(Object o) { + if( !(o instanceof Size) ) + return false; + Size that = (Size)o; + return this.width == that.width && this.height == that.height; + } + + @Override + public int hashCode() { + // must override this, as we override equals() + // can't use: + //return Objects.hash(width, height); + // as this requires API level 19 + // so use this from http://stackoverflow.com/questions/11742593/what-is-the-hashcode-for-a-custom-class-having-just-two-int-properties + return width*31 + height; + } + } + + /** An area has values from [-1000,-1000] (for top-left) to [1000,1000] (for bottom-right) for whatever is + * the current field of view (i.e., taking zoom into account). + */ + public static class Area { + final Rect rect; + final int weight; + + public Area(Rect rect, int weight) { + this.rect = rect; + this.weight = weight; + } + } + + public interface FaceDetectionListener { + void onFaceDetection(Face[] faces); + } + + public interface PictureCallback { + void onStarted(); // called immediately before we start capturing the picture + void onCompleted(); // called after all relevant on*PictureTaken() callbacks have been called and returned + void onPictureTaken(byte[] data); + /** Only called if RAW is requested. + * Caller should call image.close() and dngCreator.close() when done with the image. + */ + void onRawPictureTaken(DngCreator dngCreator, Image image); + /** Only called if burst is requested. + */ + void onBurstPictureTaken(List images); + /* This is called for flash_frontscreen_auto or flash_frontscreen_on mode to indicate the caller should light up the screen + * (for flash_frontscreen_auto it will only be called if the scene is considered dark enough to require the screen flash). + * The screen flash can be removed when or after onCompleted() is called. + */ + void onFrontScreenTurnOn(); + } + + public interface AutoFocusCallback { + void onAutoFocus(boolean success); + } + + public interface ContinuousFocusMoveCallback { + void onContinuousFocusMove(boolean start); + } + + public interface ErrorCallback { + void onError(); + } + + public static class Face { + public final int score; + /* The has values from [-1000,-1000] (for top-left) to [1000,1000] (for bottom-right) for whatever is + * the current field of view (i.e., taking zoom into account). + */ + public final Rect rect; + + Face(int score, Rect rect) { + this.score = score; + this.rect = rect; + } + } + + public static class SupportedValues { + public final List values; + public final String selected_value; + SupportedValues(List values, String selected_value) { + this.values = values; + this.selected_value = selected_value; + } + } + + public abstract void release(); + public abstract void onError(); // triggers error mechanism - should only be called externally for testing purposes + + CameraController(int cameraId) { + this.cameraId = cameraId; + } + public abstract String getAPI(); + public abstract CameraFeatures getCameraFeatures(); + public int getCameraId() { + return cameraId; + } + public abstract SupportedValues setSceneMode(String value); + /** + * @return The current scene mode. Will be null if scene mode not supported. + */ + public abstract String getSceneMode(); + public abstract SupportedValues setColorEffect(String value); + public abstract String getColorEffect(); + public abstract SupportedValues setWhiteBalance(String value); + public abstract String getWhiteBalance(); + /** Set an ISO value. Only supported if supports_iso_range is false. + */ + public abstract SupportedValues setISO(String value); + /** Switch between auto and manual ISO mode. Only supported if supports_iso_range is true. + * @param manual_iso Whether to switch to manual mode or back to auto. + * @param iso If manual_iso is true, this specifies the desired ISO value. If this is outside + * the min_iso/max_iso, the value will be snapped so it does lie within that range. + * If manual_iso i false, this value is ignored. + */ + public abstract void setManualISO(boolean manual_iso, int iso); + + /** + * @return Whether in manual ISO mode (as opposed to auto). + */ + public abstract boolean isManualISO(); + /** Specify a specific ISO value. Only supported if supports_iso_range is true. Callers should + * first switch to manual ISO mode using setManualISO(). + */ + public abstract boolean setISO(int iso); + public abstract String getISOKey(); + /** Returns the manual ISO value. Only supported if supports_iso_range is true. + */ + public abstract int getISO(); + public abstract long getExposureTime(); + public abstract boolean setExposureTime(long exposure_time); + public abstract CameraController.Size getPictureSize(); + public abstract void setPictureSize(int width, int height); + public abstract CameraController.Size getPreviewSize(); + public abstract void setPreviewSize(int width, int height); + public abstract void setExpoBracketing(boolean want_expo_bracketing); + /** n_images must be an odd number greater than 1. + */ + public abstract void setExpoBracketingNImages(int n_images); + public abstract void setExpoBracketingStops(double stops); + public abstract void setUseExpoFastBurst(boolean use_expo_fast_burst); + /** If optimise_ae_for_dro is true, then this is a hint that if in auto-exposure mode and flash/torch + * is not on, the CameraController should try to optimise for a DRO (dynamic range optimisation) mode. + */ + public abstract void setOptimiseAEForDRO(boolean optimise_ae_for_dro); + public abstract void setRaw(boolean want_raw); + /** + * setUseCamera2FakeFlash() should be called after creating the CameraController, and before calling getCameraFeatures() or + * starting the preview (as it changes the available flash modes). + * "Fake flash" is an alternative mode for handling flash, for devices that have poor Camera2 support - typical symptoms + * include precapture never starting, flash not firing, photos being over or under exposed. + * Instead, we fake the precapture and flash simply by turning on the torch. After turning on torch, we wait for ae to stop + * scanning (and af too, as it can start scanning in continuous mode) - this is effectively the equivalent of precapture - + * before taking the photo. + * In auto-focus mode, we make the decision ourselves based on the current ISO. + * We also handle the flash firing for autofocus by turning the torch on and off too. Advantages are: + * - The flash tends to be brighter, and the photo can end up overexposed as a result if capture follows the autofocus. + * - Some devices also don't seem to fire flash for autofocus in Camera2 mode (e.g., Samsung S7) + * - When capture follows autofocus, we need to make the same decision for firing flash for both the autofocus and the capture. + */ + public void setUseCamera2FakeFlash(boolean use_fake_precapture) { + } + public boolean getUseCamera2FakeFlash() { + return false; + } + public abstract void setVideoStabilization(boolean enabled); + public abstract boolean getVideoStabilization(); + public abstract int getJpegQuality(); + public abstract void setJpegQuality(int quality); + public abstract int getZoom(); + public abstract void setZoom(int value); + public abstract int getExposureCompensation(); + public abstract boolean setExposureCompensation(int new_exposure); + public abstract void setPreviewFpsRange(int min, int max); + public abstract List getSupportedPreviewFpsRange(); + + public String getDefaultSceneMode() { + return "auto"; // chosen to match Camera.Parameters.SCENE_MODE_AUTO, but we also use compatible values for Camera2 API + } + public String getDefaultColorEffect() { + return "none"; // chosen to match Camera.Parameters.EFFECT_NONE, but we also use compatible values for Camera2 API + } + public String getDefaultWhiteBalance() { + return "auto"; // chosen to match Camera.Parameters.WHITE_BALANCE_AUTO, but we also use compatible values for Camera2 API + } + public String getDefaultISO() { + return "auto"; + } + public abstract long getDefaultExposureTime(); + + public abstract void setFocusValue(String focus_value); + public abstract String getFocusValue(); + public abstract float getFocusDistance(); + public abstract boolean setFocusDistance(float focus_distance); + public abstract void setFlashValue(String flash_value); + public abstract String getFlashValue(); + public abstract void setRecordingHint(boolean hint); + public abstract void setAutoExposureLock(boolean enabled); + public abstract boolean getAutoExposureLock(); + public abstract void setRotation(int rotation); + public abstract void setLocationInfo(Location location); + public abstract void removeLocationInfo(); + public abstract void enableShutterSound(boolean enabled); + public abstract boolean setFocusAndMeteringArea(List areas); + public abstract void clearFocusAndMetering(); + public abstract List getFocusAreas(); + public abstract List getMeteringAreas(); + public abstract boolean supportsAutoFocus(); + public abstract boolean focusIsContinuous(); + public abstract boolean focusIsVideo(); + public abstract void reconnect() throws CameraControllerException; + public abstract void setPreviewDisplay(SurfaceHolder holder) throws CameraControllerException; + public abstract void setPreviewTexture(SurfaceTexture texture) throws CameraControllerException; + /** Starts the camera preview. + * @throws CameraControllerException if the camera preview fails to start. + */ + public abstract void startPreview() throws CameraControllerException; + public abstract void stopPreview(); + public abstract boolean startFaceDetection(); + public abstract void setFaceDetectionListener(final CameraController.FaceDetectionListener listener); + + /** + * @param cb Callback to be called when autofocus completes. + * @param capture_follows_autofocus_hint Set to true if you intend to take a photo immediately after autofocus. If the + * decision changes after autofocus has started (e.g., user initiates autofocus, + * then takes photo before autofocus has completed), use setCaptureFollowAutofocusHint(). + */ + public abstract void autoFocus(final CameraController.AutoFocusCallback cb, boolean capture_follows_autofocus_hint); + /** See autoFocus() for details - used to update the capture_follows_autofocus_hint setting. + */ + public abstract void setCaptureFollowAutofocusHint(boolean capture_follows_autofocus_hint); + public abstract void cancelAutoFocus(); + public abstract void setContinuousFocusMoveCallback(ContinuousFocusMoveCallback cb); + public abstract void takePicture(final CameraController.PictureCallback picture, final ErrorCallback error); + public abstract void setDisplayOrientation(int degrees); + public abstract int getDisplayOrientation(); + public abstract int getCameraOrientation(); + public abstract boolean isFrontFacing(); + public abstract void unlock(); + public abstract void initVideoRecorderPrePrepare(MediaRecorder video_recorder); + public abstract void initVideoRecorderPostPrepare(MediaRecorder video_recorder) throws CameraControllerException; + public abstract String getParametersString(); + public boolean captureResultIsAEScanning() { + return false; + } + public boolean captureResultHasIso() { + return false; + } + public int captureResultIso() { + return 0; + } + public boolean captureResultHasExposureTime() { + return false; + } + public long captureResultExposureTime() { + return 0; + } + /*public boolean captureResultHasFrameDuration() { + return false; + }*/ + /*public long captureResultFrameDuration() { + return 0; + }*/ + /*public boolean captureResultHasFocusDistance() { + return false; + }*/ + /*public float captureResultFocusDistanceMin() { + return 0.0f; + }*/ + /*public float captureResultFocusDistanceMax() { + return 0.0f; + }*/ + + // gets the available values of a generic mode, e.g., scene, color etc, and makes sure the requested mode is available + SupportedValues checkModeIsSupported(List values, String value, String default_value) { + if( values != null && values.size() > 1 ) { // n.b., if there is only 1 supported value, we also return null, as no point offering the choice to the user (there are some devices, e.g., Samsung, that only have a scene mode of "auto") + if( MyDebug.LOG ) { + for(int i=0;i convertFlashModesToValues(List supported_flash_modes) { + if( MyDebug.LOG ) { + Log.d(TAG, "convertFlashModesToValues()"); + Log.d(TAG, "supported_flash_modes: " + supported_flash_modes); + } + List output_modes = new ArrayList<>(); + if( supported_flash_modes != null ) { + // also resort as well as converting + if( supported_flash_modes.contains(Camera.Parameters.FLASH_MODE_OFF) ) { + output_modes.add("flash_off"); + if( MyDebug.LOG ) + Log.d(TAG, " supports flash_off"); + } + if( supported_flash_modes.contains(Camera.Parameters.FLASH_MODE_AUTO) ) { + output_modes.add("flash_auto"); + if( MyDebug.LOG ) + Log.d(TAG, " supports flash_auto"); + } + if( supported_flash_modes.contains(Camera.Parameters.FLASH_MODE_ON) ) { + output_modes.add("flash_on"); + if( MyDebug.LOG ) + Log.d(TAG, " supports flash_on"); + } + if( supported_flash_modes.contains(Camera.Parameters.FLASH_MODE_TORCH) ) { + output_modes.add("flash_torch"); + if( MyDebug.LOG ) + Log.d(TAG, " supports flash_torch"); + } + if( supported_flash_modes.contains(Camera.Parameters.FLASH_MODE_RED_EYE) ) { + output_modes.add("flash_red_eye"); + if( MyDebug.LOG ) + Log.d(TAG, " supports flash_red_eye"); + } + } + + // Samsung Galaxy S7 at least for front camera has supported_flash_modes: auto, beach, portrait?! + // so rather than checking supported_flash_modes, we should check output_modes here + // this is always why we check whether the size is greater than 1, rather than 0 (this also matches + // the check we do in Preview.setupCameraParameters()). + if( output_modes.size() > 1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "flash supported"); + } + else { + if( isFrontFacing() ) { + if( MyDebug.LOG ) + Log.d(TAG, "front-screen with no flash"); + output_modes.clear(); // clear any pre-existing mode (see note above about Samsung Galaxy S7) + output_modes.add("flash_off"); + output_modes.add("flash_frontscreen_on"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no flash"); + // probably best to not return any modes, rather than one mode (see note about about Samsung Galaxy S7) + output_modes.clear(); + } + } + + return output_modes; + } + + private List convertFocusModesToValues(List supported_focus_modes) { + if( MyDebug.LOG ) + Log.d(TAG, "convertFocusModesToValues()"); + List output_modes = new ArrayList<>(); + if( supported_focus_modes != null ) { + // also resort as well as converting + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_AUTO) ) { + output_modes.add("focus_mode_auto"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_auto"); + } + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY) ) { + output_modes.add("focus_mode_infinity"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_infinity"); + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_MACRO) ) { + output_modes.add("focus_mode_macro"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_macro"); + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_AUTO) ) { + output_modes.add("focus_mode_locked"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_locked"); + } + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_FIXED) ) { + output_modes.add("focus_mode_fixed"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_fixed"); + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_EDOF) ) { + output_modes.add("focus_mode_edof"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_edof"); + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) ) { + output_modes.add("focus_mode_continuous_picture"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_continuous_picture"); + } + if( supported_focus_modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) ) { + output_modes.add("focus_mode_continuous_video"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_continuous_video"); + } + } + return output_modes; + } + + public String getAPI() { + return "Camera"; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public CameraFeatures getCameraFeatures() { + if( MyDebug.LOG ) + Log.d(TAG, "getCameraFeatures()"); + Camera.Parameters parameters = this.getParameters(); + CameraFeatures camera_features = new CameraFeatures(); + camera_features.is_zoom_supported = parameters.isZoomSupported(); + if( camera_features.is_zoom_supported ) { + camera_features.max_zoom = parameters.getMaxZoom(); + try { + camera_features.zoom_ratios = parameters.getZoomRatios(); + } + catch(NumberFormatException e) { + // crash java.lang.NumberFormatException: Invalid int: " 500" reported in v1.4 on device "es209ra", Android 4.1, 3 Jan 2014 + // this is from java.lang.Integer.invalidInt(Integer.java:138) - unclear if this is a bug in Open Camera, all we can do for now is catch it + if( MyDebug.LOG ) + Log.e(TAG, "NumberFormatException in getZoomRatios()"); + e.printStackTrace(); + camera_features.is_zoom_supported = false; + camera_features.max_zoom = 0; + camera_features.zoom_ratios = null; + } + } + + camera_features.supports_face_detection = parameters.getMaxNumDetectedFaces() > 0; + + // get available sizes + List camera_picture_sizes = parameters.getSupportedPictureSizes(); + camera_features.picture_sizes = new ArrayList<>(); + //camera_features.picture_sizes.add(new CameraController.Size(1920, 1080)); // test + for(Camera.Size camera_size : camera_picture_sizes) { + camera_features.picture_sizes.add(new CameraController.Size(camera_size.width, camera_size.height)); + } + + //camera_features.supported_flash_modes = parameters.getSupportedFlashModes(); // Android format + List supported_flash_modes = parameters.getSupportedFlashModes(); // Android format + camera_features.supported_flash_values = convertFlashModesToValues(supported_flash_modes); // convert to our format (also resorts) + + List supported_focus_modes = parameters.getSupportedFocusModes(); // Android format + camera_features.supported_focus_values = convertFocusModesToValues(supported_focus_modes); // convert to our format (also resorts) + camera_features.max_num_focus_areas = parameters.getMaxNumFocusAreas(); + + camera_features.is_exposure_lock_supported = parameters.isAutoExposureLockSupported(); + + camera_features.is_video_stabilization_supported = parameters.isVideoStabilizationSupported(); + + camera_features.min_exposure = parameters.getMinExposureCompensation(); + camera_features.max_exposure = parameters.getMaxExposureCompensation(); + try { + camera_features.exposure_step = parameters.getExposureCompensationStep(); + } + catch(Exception e) { + // received a NullPointerException from StringToReal.parseFloat() beneath getExposureCompensationStep() on Google Play! + if( MyDebug.LOG ) + Log.e(TAG, "exception from getExposureCompensationStep()"); + e.printStackTrace(); + camera_features.exposure_step = 1.0f/3.0f; // make up a typical example + } + + List camera_video_sizes = parameters.getSupportedVideoSizes(); + if( camera_video_sizes == null ) { + // if null, we should use the preview sizes - see http://stackoverflow.com/questions/14263521/android-getsupportedvideosizes-allways-returns-null + if( MyDebug.LOG ) + Log.d(TAG, "take video_sizes from preview sizes"); + camera_video_sizes = parameters.getSupportedPreviewSizes(); + } + camera_features.video_sizes = new ArrayList<>(); + //camera_features.video_sizes.add(new CameraController.Size(1920, 1080)); // test + for(Camera.Size camera_size : camera_video_sizes) { + camera_features.video_sizes.add(new CameraController.Size(camera_size.width, camera_size.height)); + } + + List camera_preview_sizes = parameters.getSupportedPreviewSizes(); + camera_features.preview_sizes = new ArrayList<>(); + for(Camera.Size camera_size : camera_preview_sizes) { + camera_features.preview_sizes.add(new CameraController.Size(camera_size.width, camera_size.height)); + } + + if( MyDebug.LOG ) + Log.d(TAG, "camera parameters: " + parameters.flatten()); + + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) { + // Camera.canDisableShutterSound requires JELLY_BEAN_MR1 or greater + camera_features.can_disable_shutter_sound = camera_info.canDisableShutterSound; + } + else { + camera_features.can_disable_shutter_sound = false; + } + + // Determine view angles. Note that these can vary based on the resolution - and since we read these before the caller has + // set the desired resolution, this isn't strictly correct. However these are presumably view angles for the photo anyway, + // when some callers (e.g., DrawPreview) want view angles for the preview anyway - so these will only be an approximation for + // what we want anyway. + final float default_view_angle_x = 55.0f; + final float default_view_angle_y = 43.0f; + try { + camera_features.view_angle_x = parameters.getHorizontalViewAngle(); + camera_features.view_angle_y = parameters.getVerticalViewAngle(); + } + catch(Exception e) { + // apparently some devices throw exceptions... + e.printStackTrace(); + Log.e(TAG, "exception reading horizontal or vertical view angles"); + camera_features.view_angle_x = default_view_angle_x; + camera_features.view_angle_y = default_view_angle_y; + } + if( MyDebug.LOG ) { + Log.d(TAG, "view_angle_x: " + camera_features.view_angle_x); + Log.d(TAG, "view_angle_y: " + camera_features.view_angle_y); + } + // need to handle some devices reporting rubbish + if( camera_features.view_angle_x > 150.0f || camera_features.view_angle_y > 150.0f ) { + Log.e(TAG, "camera API reporting stupid view angles, set to sensible defaults"); + camera_features.view_angle_x = default_view_angle_x; + camera_features.view_angle_y = default_view_angle_y; + } + + return camera_features; + } + + public long getDefaultExposureTime() { + // not supported for CameraController1 + return 0L; + } + + // important, from docs: + // "Changing scene mode may override other parameters (such as flash mode, focus mode, white balance). + // For example, suppose originally flash mode is on and supported flash modes are on/off. In night + // scene mode, both flash mode and supported flash mode may be changed to off. After setting scene + // mode, applications should call getParameters to know if some parameters are changed." + public SupportedValues setSceneMode(String value) { + String default_value = getDefaultSceneMode(); + Camera.Parameters parameters = this.getParameters(); + List values = parameters.getSupportedSceneModes(); + /*{ + // test + values = new ArrayList<>(); + values.add("auto"); + }*/ + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + String scene_mode = parameters.getSceneMode(); + // if scene mode is null, it should mean scene modes aren't supported anyway + if( scene_mode != null && !scene_mode.equals(supported_values.selected_value) ) { + parameters.setSceneMode(supported_values.selected_value); + setCameraParameters(parameters); + } + } + return supported_values; + } + + public String getSceneMode() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getSceneMode(); + } + + public SupportedValues setColorEffect(String value) { + String default_value = getDefaultColorEffect(); + Camera.Parameters parameters = this.getParameters(); + List values = parameters.getSupportedColorEffects(); + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + if( !parameters.getColorEffect().equals(supported_values.selected_value) ) { + parameters.setColorEffect(supported_values.selected_value); + setCameraParameters(parameters); + } + } + return supported_values; + } + + public String getColorEffect() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getColorEffect(); + } + + public SupportedValues setWhiteBalance(String value) { + String default_value = getDefaultWhiteBalance(); + Camera.Parameters parameters = this.getParameters(); + List values = parameters.getSupportedWhiteBalance(); + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + if( !parameters.getWhiteBalance().equals(supported_values.selected_value) ) { + parameters.setWhiteBalance(supported_values.selected_value); + setCameraParameters(parameters); + } + } + return supported_values; + } + + public String getWhiteBalance() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getWhiteBalance(); + } + + @Override + public SupportedValues setISO(String value) { + Camera.Parameters parameters = this.getParameters(); + // get available isos - no standard value for this, see http://stackoverflow.com/questions/2978095/android-camera-api-iso-setting + String iso_values = parameters.get("iso-values"); + if( iso_values == null ) { + iso_values = parameters.get("iso-mode-values"); // Galaxy Nexus + if( iso_values == null ) { + iso_values = parameters.get("iso-speed-values"); // Micromax A101 + if( iso_values == null ) + iso_values = parameters.get("nv-picture-iso-values"); // LG dual P990 + } + } + List values = null; + if( iso_values != null && iso_values.length() > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "iso_values: " + iso_values); + String [] isos_array = iso_values.split(","); + // split shouldn't return null + if( isos_array.length > 0 ) { + // remove duplicates (OnePlus 3T has several duplicate "auto" entries) + HashSet hashSet = new HashSet<>(); + values = new ArrayList<>(); + // use hashset for efficiency + // make sure we alo preserve the order + for(String iso : isos_array) { + if( !hashSet.contains(iso) ) { + values.add(iso); + hashSet.add(iso); + } + } + } + } + + iso_key = "iso"; + if( parameters.get(iso_key) == null ) { + iso_key = "iso-speed"; // Micromax A101 + if( parameters.get(iso_key) == null ) { + iso_key = "nv-picture-iso"; // LG dual P990 + if( parameters.get(iso_key) == null ) { + if ( Build.MODEL.contains("Z00") ) + iso_key = "iso"; // Asus Zenfone 2 Z00A and Z008: see https://sourceforge.net/p/opencamera/tickets/183/ + else + iso_key = null; // not supported + } + } + } + /*values = new ArrayList<>(); + //values.add("auto"); + //values.add("ISO_HJR"); + values.add("ISO50"); + values.add("ISO64"); + values.add("ISO80"); + values.add("ISO100"); + values.add("ISO125"); + values.add("ISO160"); + values.add("ISO200"); + values.add("ISO250"); + values.add("ISO320"); + values.add("ISO400"); + values.add("ISO500"); + values.add("ISO640"); + values.add("ISO800"); + values.add("ISO1000"); + values.add("ISO1250"); + values.add("ISO1600"); + values.add("ISO2000"); + values.add("ISO2500"); + values.add("ISO3200"); + values.add("auto"); + //values.add("400"); + //values.add("800"); + //values.add("1600"); + iso_key = "iso";*/ + if( iso_key != null ){ + if( values == null ) { + // set a default for some devices which have an iso_key, but don't give a list of supported ISOs + values = new ArrayList<>(); + values.add("auto"); + values.add("50"); + values.add("100"); + values.add("200"); + values.add("400"); + values.add("800"); + values.add("1600"); + } + SupportedValues supported_values = checkModeIsSupported(values, value, getDefaultISO()); + if( supported_values != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "set: " + iso_key + " to: " + supported_values.selected_value); + parameters.set(iso_key, supported_values.selected_value); + setCameraParameters(parameters); + } + return supported_values; + } + return null; + } + + @Override + public String getISOKey() { + if( MyDebug.LOG ) + Log.d(TAG, "getISOKey"); + return this.iso_key; + } + + @Override + public void setManualISO(boolean manual_iso, int iso) { + // not supported for CameraController1 + } + + @Override + public boolean isManualISO() { + // not supported for CameraController1 + return false; + } + + @Override + public boolean setISO(int iso) { + // not supported for CameraController1 + return false; + } + + @Override + public int getISO() { + // not supported for CameraController1 + return 0; + } + + @Override + public long getExposureTime() { + // not supported for CameraController1 + return 0L; + } + + @Override + public boolean setExposureTime(long exposure_time) { + // not supported for CameraController1 + return false; + } + + @Override + public CameraController.Size getPictureSize() { + Camera.Parameters parameters = this.getParameters(); + Camera.Size camera_size = parameters.getPictureSize(); + return new CameraController.Size(camera_size.width, camera_size.height); + } + + @Override + public void setPictureSize(int width, int height) { + Camera.Parameters parameters = this.getParameters(); + parameters.setPictureSize(width, height); + if( MyDebug.LOG ) + Log.d(TAG, "set picture size: " + parameters.getPictureSize().width + ", " + parameters.getPictureSize().height); + setCameraParameters(parameters); + } + + @Override + public CameraController.Size getPreviewSize() { + Camera.Parameters parameters = this.getParameters(); + Camera.Size camera_size = parameters.getPreviewSize(); + return new CameraController.Size(camera_size.width, camera_size.height); + } + + @Override + public void setPreviewSize(int width, int height) { + Camera.Parameters parameters = this.getParameters(); + if( MyDebug.LOG ) + Log.d(TAG, "current preview size: " + parameters.getPreviewSize().width + ", " + parameters.getPreviewSize().height); + parameters.setPreviewSize(width, height); + if( MyDebug.LOG ) + Log.d(TAG, "new preview size: " + parameters.getPreviewSize().width + ", " + parameters.getPreviewSize().height); + setCameraParameters(parameters); + } + + @Override + public void setExpoBracketing(boolean want_expo_bracketing) { + // not supported for CameraController1 + } + + @Override + public void setExpoBracketingNImages(int n_images) { + // not supported for CameraController1 + } + + @Override + public void setExpoBracketingStops(double stops) { + // not supported for CameraController1 + } + + @Override + public void setUseExpoFastBurst(boolean use_expo_fast_burst) { + // not supported for CameraController1 + } + + @Override + public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { + // not supported for CameraController1 + } + + @Override + public void setRaw(boolean want_raw) { + // not supported for CameraController1 + } + + @Override + public void setVideoStabilization(boolean enabled) { + Camera.Parameters parameters = this.getParameters(); + parameters.setVideoStabilization(enabled); + setCameraParameters(parameters); + } + + public boolean getVideoStabilization() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getVideoStabilization(); + } + + public int getJpegQuality() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getJpegQuality(); + } + + public void setJpegQuality(int quality) { + Camera.Parameters parameters = this.getParameters(); + parameters.setJpegQuality(quality); + setCameraParameters(parameters); + } + + public int getZoom() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getZoom(); + } + + public void setZoom(int value) { + Camera.Parameters parameters = this.getParameters(); + if( MyDebug.LOG ) + Log.d(TAG, "zoom was: " + parameters.getZoom()); + parameters.setZoom(value); + setCameraParameters(parameters); + } + + public int getExposureCompensation() { + Camera.Parameters parameters = this.getParameters(); + return parameters.getExposureCompensation(); + } + + // Returns whether exposure was modified + public boolean setExposureCompensation(int new_exposure) { + Camera.Parameters parameters = this.getParameters(); + int current_exposure = parameters.getExposureCompensation(); + if( new_exposure != current_exposure ) { + if( MyDebug.LOG ) + Log.d(TAG, "change exposure from " + current_exposure + " to " + new_exposure); + parameters.setExposureCompensation(new_exposure); + setCameraParameters(parameters); + return true; + } + return false; + } + + public void setPreviewFpsRange(int min, int max) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewFpsRange: " + min + " to " + max); + Camera.Parameters parameters = this.getParameters(); + parameters.setPreviewFpsRange(min, max); + setCameraParameters(parameters); + } + + public List getSupportedPreviewFpsRange() { + Camera.Parameters parameters = this.getParameters(); + try { + return parameters.getSupportedPreviewFpsRange(); + } + catch(StringIndexOutOfBoundsException e) { + /* Have had reports of StringIndexOutOfBoundsException on Google Play on Sony Xperia M devices + at android.hardware.Camera$Parameters.splitRange(Camera.java:4098) + at android.hardware.Camera$Parameters.getSupportedPreviewFpsRange(Camera.java:2799) + */ + e.printStackTrace(); + if( MyDebug.LOG ) { + Log.e(TAG, "getSupportedPreviewFpsRange() gave StringIndexOutOfBoundsException"); + } + } + return null; + } + + @Override + public void setFocusValue(String focus_value) { + Camera.Parameters parameters = this.getParameters(); + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_locked") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + } + else if( focus_value.equals("focus_mode_infinity") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); + } + else if( focus_value.equals("focus_mode_macro") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO); + } + else if( focus_value.equals("focus_mode_fixed") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); + } + else if( focus_value.equals("focus_mode_edof") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_EDOF); + } + else if( focus_value.equals("focus_mode_continuous_picture") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } + else if( focus_value.equals("focus_mode_continuous_video") ) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusValue() received unknown focus value " + focus_value); + } + setCameraParameters(parameters); + } + + private String convertFocusModeToValue(String focus_mode) { + // focus_mode may be null on some devices; we return "" + if( MyDebug.LOG ) + Log.d(TAG, "convertFocusModeToValue: " + focus_mode); + String focus_value = ""; + if( focus_mode == null ) { + // ignore, leave focus_value at "" + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_AUTO) ) { + focus_value = "focus_mode_auto"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_INFINITY) ) { + focus_value = "focus_mode_infinity"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_MACRO) ) { + focus_value = "focus_mode_macro"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_FIXED) ) { + focus_value = "focus_mode_fixed"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_EDOF) ) { + focus_value = "focus_mode_edof"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) ) { + focus_value = "focus_mode_continuous_picture"; + } + else if( focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) ) { + focus_value = "focus_mode_continuous_video"; + } + return focus_value; + } + + @Override + public String getFocusValue() { + // returns "" if Parameters.getFocusMode() returns null + Camera.Parameters parameters = this.getParameters(); + String focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play + return convertFocusModeToValue(focus_mode); + } + + @Override + public float getFocusDistance() { + // not supported for CameraController1! + return 0.0f; + } + + @Override + public boolean setFocusDistance(float focus_distance) { + // not supported for CameraController1! + return false; + } + + private String convertFlashValueToMode(String flash_value) { + String flash_mode = ""; + if( flash_value.equals("flash_off") ) { + flash_mode = Camera.Parameters.FLASH_MODE_OFF; + } + else if( flash_value.equals("flash_auto") ) { + flash_mode = Camera.Parameters.FLASH_MODE_AUTO; + } + else if( flash_value.equals("flash_on") ) { + flash_mode = Camera.Parameters.FLASH_MODE_ON; + } + else if( flash_value.equals("flash_torch") ) { + flash_mode = Camera.Parameters.FLASH_MODE_TORCH; + } + else if( flash_value.equals("flash_red_eye") ) { + flash_mode = Camera.Parameters.FLASH_MODE_RED_EYE; + } + else if( flash_value.equals("flash_frontscreen_on") ) { + flash_mode = Camera.Parameters.FLASH_MODE_OFF; + } + return flash_mode; + } + + public void setFlashValue(String flash_value) { + Camera.Parameters parameters = this.getParameters(); + if( MyDebug.LOG ) + Log.d(TAG, "setFlashValue: " + flash_value); + + this.frontscreen_flash = false; + if( flash_value.equals("flash_frontscreen_on") ) { + // we do this check first due to weird behaviour on Samsung Galaxy S7 front camera where parameters.getFlashMode() returns values (auto, beach, portrait) + this.frontscreen_flash = true; + return; + } + + if( parameters.getFlashMode() == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "flash mode not supported"); + return; + } + + final String flash_mode = convertFlashValueToMode(flash_value); + if( flash_mode.length() > 0 && !flash_mode.equals(parameters.getFlashMode()) ) { + if( parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH) && !flash_mode.equals(Camera.Parameters.FLASH_MODE_OFF) ) { + // workaround for bug on Nexus 5 and Nexus 6 where torch doesn't switch off until we set FLASH_MODE_OFF + if( MyDebug.LOG ) + Log.d(TAG, "first turn torch off"); + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + setCameraParameters(parameters); + // need to set the correct flash mode after a delay + Handler handler = new Handler(); + handler.postDelayed(new Runnable(){ + @Override + public void run(){ + if( MyDebug.LOG ) + Log.d(TAG, "now set actual flash mode after turning torch off"); + if( camera != null ) { // make sure camera wasn't released in the meantime (has a Google Play crash as a result of this) + Camera.Parameters parameters = getParameters(); + parameters.setFlashMode(flash_mode); + setCameraParameters(parameters); + } + } + }, 100); + } + else { + parameters.setFlashMode(flash_mode); + setCameraParameters(parameters); + } + } + } + + private String convertFlashModeToValue(String flash_mode) { + // flash_mode may be null, meaning flash isn't supported; we return "" + if( MyDebug.LOG ) + Log.d(TAG, "convertFlashModeToValue: " + flash_mode); + String flash_value = ""; + if( flash_mode == null ) { + // ignore, leave focus_value at "" + } + else if( flash_mode.equals(Camera.Parameters.FLASH_MODE_OFF) ) { + flash_value = "flash_off"; + } + else if( flash_mode.equals(Camera.Parameters.FLASH_MODE_AUTO) ) { + flash_value = "flash_auto"; + } + else if( flash_mode.equals(Camera.Parameters.FLASH_MODE_ON) ) { + flash_value = "flash_on"; + } + else if( flash_mode.equals(Camera.Parameters.FLASH_MODE_TORCH) ) { + flash_value = "flash_torch"; + } + else if( flash_mode.equals(Camera.Parameters.FLASH_MODE_RED_EYE) ) { + flash_value = "flash_red_eye"; + } + return flash_value; + } + + public String getFlashValue() { + // returns "" if flash isn't supported + Camera.Parameters parameters = this.getParameters(); + String flash_mode = parameters.getFlashMode(); // will be null if flash mode not supported + return convertFlashModeToValue(flash_mode); + } + + public void setRecordingHint(boolean hint) { + if( MyDebug.LOG ) + Log.d(TAG, "setRecordingHint: " + hint); + Camera.Parameters parameters = this.getParameters(); + // Calling setParameters here with continuous video focus mode causes preview to not restart after taking a photo on Galaxy Nexus?! (fine on my Nexus 7). + // The issue seems to specifically be with setParameters (i.e., the problem occurs even if we don't setRecordingHint). + // In addition, I had a report of a bug on HTC Desire X, Android 4.0.4 where the saved video was corrupted. + // This worked fine in 1.7, then not in 1.8 and 1.9, then was fixed again in 1.10 + // The only thing in common to 1.7->1.8 and 1.9-1.10, that seems relevant, was adding this code to setRecordingHint() and setParameters() (unclear which would have been the problem), + // so we should be very careful about enabling this code again! + // Update for v1.23: the bug with Galaxy Nexus has come back (see comments in Preview.setPreviewFps()) and is now unavoidable, + // but I've still kept this check here - if nothing else, because it apparently caused video recording problems on other devices too. + // Update for v1.29: this doesn't seem to happen on Galaxy Nexus with continuous picture focus mode, which is what we now use; but again, still keepin the check here due to possible problems on other devices + String focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play + if( focus_mode != null && !focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) ) { + parameters.setRecordingHint(hint); + setCameraParameters(parameters); + } + } + + public void setAutoExposureLock(boolean enabled) { + Camera.Parameters parameters = this.getParameters(); + parameters.setAutoExposureLock(enabled); + setCameraParameters(parameters); + } + + public boolean getAutoExposureLock() { + Camera.Parameters parameters = this.getParameters(); + if( !parameters.isAutoExposureLockSupported() ) + return false; + return parameters.getAutoExposureLock(); + } + + public void setRotation(int rotation) { + Camera.Parameters parameters = this.getParameters(); + parameters.setRotation(rotation); + setCameraParameters(parameters); + } + + public void setLocationInfo(Location location) { + Camera.Parameters parameters = this.getParameters(); + parameters.removeGpsData(); + parameters.setGpsTimestamp(System.currentTimeMillis() / 1000); // initialise to a value (from Android camera source) + parameters.setGpsLatitude(location.getLatitude()); + parameters.setGpsLongitude(location.getLongitude()); + parameters.setGpsProcessingMethod(location.getProvider()); // from http://boundarydevices.com/how-to-write-an-android-camera-app/ + if( location.hasAltitude() ) { + parameters.setGpsAltitude(location.getAltitude()); + } + else { + // Android camera source claims we need to fake one if not present + // and indeed, this is needed to fix crash on Nexus 7 + parameters.setGpsAltitude(0); + } + if( location.getTime() != 0 ) { // from Android camera source + parameters.setGpsTimestamp(location.getTime() / 1000); + } + setCameraParameters(parameters); + } + + public void removeLocationInfo() { + Camera.Parameters parameters = this.getParameters(); + parameters.removeGpsData(); + setCameraParameters(parameters); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void enableShutterSound(boolean enabled) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) { + camera.enableShutterSound(enabled); + } + } + + public boolean setFocusAndMeteringArea(List areas) { + List camera_areas = new ArrayList<>(); + for(CameraController.Area area : areas) { + camera_areas.add(new Camera.Area(area.rect, area.weight)); + } + Camera.Parameters parameters = this.getParameters(); + String focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play + if( parameters.getMaxNumFocusAreas() != 0 && focus_mode != null && ( focus_mode.equals(Camera.Parameters.FOCUS_MODE_AUTO) || focus_mode.equals(Camera.Parameters.FOCUS_MODE_MACRO) || focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) || focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) ) ) { + parameters.setFocusAreas(camera_areas); + + // also set metering areas + if( parameters.getMaxNumMeteringAreas() == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "metering areas not supported"); + } + else { + parameters.setMeteringAreas(camera_areas); + } + + setCameraParameters(parameters); + + return true; + } + else if( parameters.getMaxNumMeteringAreas() != 0 ) { + parameters.setMeteringAreas(camera_areas); + + setCameraParameters(parameters); + } + return false; + } + + public void clearFocusAndMetering() { + Camera.Parameters parameters = this.getParameters(); + boolean update_parameters = false; + if( parameters.getMaxNumFocusAreas() > 0 ) { + parameters.setFocusAreas(null); + update_parameters = true; + } + if( parameters.getMaxNumMeteringAreas() > 0 ) { + parameters.setMeteringAreas(null); + update_parameters = true; + } + if( update_parameters ) { + setCameraParameters(parameters); + } + } + + public List getFocusAreas() { + Camera.Parameters parameters = this.getParameters(); + List camera_areas = parameters.getFocusAreas(); + if( camera_areas == null ) + return null; + List areas = new ArrayList<>(); + for(Camera.Area camera_area : camera_areas) { + areas.add(new CameraController.Area(camera_area.rect, camera_area.weight)); + } + return areas; + } + + public List getMeteringAreas() { + Camera.Parameters parameters = this.getParameters(); + List camera_areas = parameters.getMeteringAreas(); + if( camera_areas == null ) + return null; + List areas = new ArrayList<>(); + for(Camera.Area camera_area : camera_areas) { + areas.add(new CameraController.Area(camera_area.rect, camera_area.weight)); + } + return areas; + } + + @Override + public boolean supportsAutoFocus() { + Camera.Parameters parameters = this.getParameters(); + String focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play from the below line (v1.7), + // on Galaxy Tab 10.1 (GT-P7500), Android 4.0.3 - 4.0.4; HTC EVO 3D X515m (shooteru), Android 4.0.3 - 4.0.4 + if( focus_mode != null && ( focus_mode.equals(Camera.Parameters.FOCUS_MODE_AUTO) || focus_mode.equals(Camera.Parameters.FOCUS_MODE_MACRO) ) ) { + return true; + } + return false; + } + + @Override + public boolean focusIsContinuous() { + Camera.Parameters parameters = this.getParameters(); + String focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play from the below line (v1.7), + // on Galaxy Tab 10.1 (GT-P7500), Android 4.0.3 - 4.0.4; HTC EVO 3D X515m (shooteru), Android 4.0.3 - 4.0.4 + if( focus_mode != null && ( focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) || focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) ) ) { + return true; + } + return false; + } + + public boolean focusIsVideo() { + Camera.Parameters parameters = this.getParameters(); + String current_focus_mode = parameters.getFocusMode(); + // getFocusMode() is documented as never returning null, however I've had null pointer exceptions reported in Google Play + boolean focus_is_video = current_focus_mode != null && current_focus_mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + if( MyDebug.LOG ) { + Log.d(TAG, "current_focus_mode: " + current_focus_mode); + Log.d(TAG, "focus_is_video: " + focus_is_video); + } + return focus_is_video; + } + + @Override + public + void reconnect() throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "reconnect"); + try { + camera.reconnect(); + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "reconnect threw IOException"); + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void setPreviewDisplay(SurfaceHolder holder) throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewDisplay"); + try { + camera.setPreviewDisplay(holder); + } + catch(IOException e) { + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void setPreviewTexture(SurfaceTexture texture) throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewTexture"); + try { + camera.setPreviewTexture(texture); + } + catch(IOException e) { + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void startPreview() throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "startPreview"); + try { + camera.startPreview(); + } + catch(RuntimeException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to start preview"); + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void stopPreview() { + camera.stopPreview(); + } + + // returns false if RuntimeException thrown (may include if face-detection already started) + public boolean startFaceDetection() { + try { + camera.startFaceDetection(); + } + catch(RuntimeException e) { + if( MyDebug.LOG ) + Log.d(TAG, "face detection failed or already started"); + return false; + } + return true; + } + + public void setFaceDetectionListener(final CameraController.FaceDetectionListener listener) { + class CameraFaceDetectionListener implements Camera.FaceDetectionListener { + @Override + public void onFaceDetection(Camera.Face[] camera_faces, Camera camera) { + Face [] faces = new Face[camera_faces.length]; + for(int i=0;i= Build.VERSION_CODES.JELLY_BEAN ) { + // setAutoFocusMoveCallback() requires JELLY_BEAN + try { + if( cb != null ) { + camera.setAutoFocusMoveCallback(new AutoFocusMoveCallback() { + @Override + public void onAutoFocusMoving(boolean start, Camera camera) { + if( MyDebug.LOG ) + Log.d(TAG, "onAutoFocusMoving: " + start); + cb.onContinuousFocusMove(start); + } + }); + } + else { + camera.setAutoFocusMoveCallback(null); + } + } + catch(RuntimeException e) { + // received RuntimeException reports from some users on Google Play - seems to be older devices, but still important to catch! + if( MyDebug.LOG ) + Log.e(TAG, "runtime exception from setAutoFocusMoveCallback"); + e.printStackTrace(); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "setContinuousFocusMoveCallback requires Android JELLY_BEAN or higher"); + } + } + + private static class TakePictureShutterCallback implements Camera.ShutterCallback { + // don't do anything here, but we need to implement the callback to get the shutter sound (at least on Galaxy Nexus and Nexus 7) + @Override + public void onShutter() { + if( MyDebug.LOG ) + Log.d(TAG, "shutterCallback.onShutter()"); + } + } + + private void takePictureNow(final CameraController.PictureCallback picture, final ErrorCallback error) { + if( MyDebug.LOG ) + Log.d(TAG, "takePictureNow"); + Camera.ShutterCallback shutter = new TakePictureShutterCallback(); + Camera.PictureCallback camera_jpeg = picture == null ? null : new Camera.PictureCallback() { + public void onPictureTaken(byte[] data, Camera cam) { + // n.b., this is automatically run in a different thread + picture.onPictureTaken(data); + picture.onCompleted(); + } + }; + + if( picture != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "call onStarted() in callback"); + picture.onStarted(); + } + try { + camera.takePicture(shutter, null, camera_jpeg); + } + catch(RuntimeException e) { + // just in case? We got a RuntimeException report here from 1 user on Google Play; I also encountered it myself once of Galaxy Nexus when starting up + if( MyDebug.LOG ) + Log.e(TAG, "runtime exception from takePicture"); + e.printStackTrace(); + error.onError(); + } + } + + public void takePicture(final CameraController.PictureCallback picture, final ErrorCallback error) { + if( MyDebug.LOG ) + Log.d(TAG, "takePicture"); + if( frontscreen_flash ) { + if( MyDebug.LOG ) + Log.d(TAG, "front screen flash"); + picture.onFrontScreenTurnOn(); + // take picture after a delay, to allow autoexposure and autofocus to update (unlike CameraController2, we can't tell when this happens, so we just wait for a fixed delay) + Handler handler = new Handler(); + handler.postDelayed(new Runnable(){ + @Override + public void run(){ + if( MyDebug.LOG ) + Log.d(TAG, "take picture after delay for front screen flash"); + if( camera != null ) { // make sure camera wasn't released in the meantime + takePictureNow(picture, error); + } + } + }, 1000); + return; + } + takePictureNow(picture, error); + } + + public void setDisplayOrientation(int degrees) { + // rest of code from http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) + int result; + if( camera_info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT ) { + result = (camera_info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } + else { + result = (camera_info.orientation - degrees + 360) % 360; + } + if( MyDebug.LOG ) { + Log.d(TAG, " info orientation is " + camera_info.orientation); + Log.d(TAG, " setDisplayOrientation to " + result); + } + + camera.setDisplayOrientation(result); + this.display_orientation = result; + } + + public int getDisplayOrientation() { + return this.display_orientation; + } + + public int getCameraOrientation() { + return camera_info.orientation; + } + + public boolean isFrontFacing() { + return (camera_info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + public void unlock() { + this.stopPreview(); // although not documented, we need to stop preview to prevent device freeze or video errors shortly after video recording starts on some devices (e.g., device freeze on Samsung Galaxy S2 - I could reproduce this on Samsung RTL; also video recording fails and preview becomes corrupted on Galaxy S3 variant "SGH-I747-US2"); also see http://stackoverflow.com/questions/4244999/problem-with-video-recording-after-auto-focus-in-android + camera.unlock(); + } + + @Override + public void initVideoRecorderPrePrepare(MediaRecorder video_recorder) { + video_recorder.setCamera(camera); + } + + @Override + public void initVideoRecorderPostPrepare(MediaRecorder video_recorder) throws CameraControllerException { + // no further actions necessary + } + + @Override + public String getParametersString() { + String string = ""; + try { + string = this.getParameters().flatten(); + } + catch(Exception e) { + // received a StringIndexOutOfBoundsException from beneath getParameters().flatten() on Google Play! + if( MyDebug.LOG ) + Log.e(TAG, "exception from getParameters().flatten()"); + e.printStackTrace(); + } + return string; + } +} diff --git a/src/main/java/net/sourceforge/opencamera/CameraController/CameraController2.java b/src/main/java/net/sourceforge/opencamera/CameraController/CameraController2.java new file mode 100644 index 00000000..ddc1a572 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/CameraController/CameraController2.java @@ -0,0 +1,4441 @@ +package net.sourceforge.opencamera.CameraController; + +import net.sourceforge.opencamera.MyDebug; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.DngCreator; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.location.Location; +import android.media.ExifInterface; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaActionSound; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.annotation.NonNull; +import android.util.Log; +import android.util.Range; +import android.util.SizeF; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; + +/** Provides support using Android 5's Camera 2 API + * android.hardware.camera2.*. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class CameraController2 extends CameraController { + private static final String TAG = "CameraController2"; + + private final Context context; + private CameraDevice camera; + private String cameraIdS; + private CameraCharacteristics characteristics; + private List zoom_ratios; + private int current_zoom_value; + private final ErrorCallback preview_error_cb; + private final ErrorCallback camera_error_cb; + private CameraCaptureSession captureSession; + private CaptureRequest.Builder previewBuilder; + private AutoFocusCallback autofocus_cb; + private boolean capture_follows_autofocus_hint; + private FaceDetectionListener face_detection_listener; + private final Object image_reader_lock = new Object(); // lock to make sure we only handle one image being available at a time + private final Object open_camera_lock = new Object(); // lock to wait for camera to be opened from CameraDevice.StateCallback + private final Object create_capture_session_lock = new Object(); // lock to wait for capture session to be created from CameraCaptureSession.StateCallback + private ImageReader imageReader; + private boolean want_expo_bracketing; + private int expo_bracketing_n_images = 3; + private double expo_bracketing_stops = 2.0; + private boolean use_expo_fast_burst = true; + private boolean optimise_ae_for_dro = false; + private boolean want_raw; + //private boolean want_raw = true; + private android.util.Size raw_size; + private ImageReader imageReaderRaw; + private OnRawImageAvailableListener onRawImageAvailableListener; + private PictureCallback jpeg_cb; + private PictureCallback raw_cb; + private int n_burst; // number of expected burst images in this capture + private final List pending_burst_images = new ArrayList<>(); // burst images that have been captured so far, but not yet sent to the application + private List burst_capture_requests; // the set of burst capture requests - used when not using captureBurst() (i.e., when use_expo_fast_burst==false) + private long burst_start_ms = 0; // time when burst started (used for measuring performance of captures when not using fast burst) + private DngCreator pending_dngCreator; + private Image pending_image; + private ErrorCallback take_picture_error_cb; + //private ImageReader previewImageReader; + private SurfaceTexture texture; + private Surface surface_texture; + private HandlerThread thread; + private Handler handler; + + private int preview_width; + private int preview_height; + + private int picture_width; + private int picture_height; + + private static final int STATE_NORMAL = 0; + private static final int STATE_WAITING_AUTOFOCUS = 1; + private static final int STATE_WAITING_PRECAPTURE_START = 2; + private static final int STATE_WAITING_PRECAPTURE_DONE = 3; + private static final int STATE_WAITING_FAKE_PRECAPTURE_START = 4; + private static final int STATE_WAITING_FAKE_PRECAPTURE_DONE = 5; + private int state = STATE_NORMAL; + private long precapture_state_change_time_ms = -1; // time we changed state for precapture modes + private static final long precapture_start_timeout_c = 2000; + private static final long precapture_done_timeout_c = 3000; + private boolean ready_for_capture; + + private boolean use_fake_precapture; // see CameraController.setUseCamera2FakeFlash() for details - this is the user/application setting, see use_fake_precapture_mode for whether fake precapture is enabled (as we may do this for other purposes, e.g., front screen flash) + private boolean use_fake_precapture_mode; // true if either use_fake_precapture is true, or we're temporarily using fake precapture mode (e.g., for front screen flash or exposure bracketing) + private boolean fake_precapture_torch_performed; // whether we turned on torch to do a fake precapture + private boolean fake_precapture_torch_focus_performed; // whether we turned on torch to do an autofocus, in fake precapture mode + private boolean fake_precapture_use_flash; // whether we decide to use flash in auto mode (if fake_precapture_use_autoflash_time_ms != -1) + private long fake_precapture_use_flash_time_ms = -1; // when we last checked to use flash in auto mode + + private ContinuousFocusMoveCallback continuous_focus_move_callback; + + private final MediaActionSound media_action_sound = new MediaActionSound(); + private boolean sounds_enabled = true; + + private boolean capture_result_is_ae_scanning; + private boolean capture_result_needs_flash; // whether flash will fire + private boolean capture_result_has_iso; + private int capture_result_iso; + private boolean capture_result_has_exposure_time; + private long capture_result_exposure_time; + private boolean capture_result_has_frame_duration; + private long capture_result_frame_duration ; + /*private boolean capture_result_has_focus_distance; + private float capture_result_focus_distance_min; + private float capture_result_focus_distance_max;*/ + + private enum RequestTag { + CAPTURE + } + + private class CameraSettings { + // keys that we need to store, to pass to the stillBuilder, but doesn't need to be passed to previewBuilder (should set sensible defaults) + private int rotation; + private Location location; + private byte jpeg_quality = 90; + + // keys that we have passed to the previewBuilder, that we need to store to also pass to the stillBuilder (should set sensible defaults, or use a has_ boolean if we don't want to set a default) + private int scene_mode = CameraMetadata.CONTROL_SCENE_MODE_DISABLED; + private int color_effect = CameraMetadata.CONTROL_EFFECT_MODE_OFF; + private int white_balance = CameraMetadata.CONTROL_AWB_MODE_AUTO; + private String flash_value = "flash_off"; + private boolean has_iso; + //private int ae_mode = CameraMetadata.CONTROL_AE_MODE_ON; + //private int flash_mode = CameraMetadata.FLASH_MODE_OFF; + private int iso; + private long exposure_time = EXPOSURE_TIME_DEFAULT; + private Rect scalar_crop_region; // no need for has_scalar_crop_region, as we can set to null instead + private boolean has_ae_exposure_compensation; + private int ae_exposure_compensation; + private boolean has_af_mode; + private int af_mode = CaptureRequest.CONTROL_AF_MODE_AUTO; + private float focus_distance; // actual value passed to camera device (set to 0.0 if in infinity mode) + private float focus_distance_manual; // saved setting when in manual mode + private boolean ae_lock; + private MeteringRectangle [] af_regions; // no need for has_scalar_crop_region, as we can set to null instead + private MeteringRectangle [] ae_regions; // no need for has_scalar_crop_region, as we can set to null instead + private boolean has_face_detect_mode; + private int face_detect_mode = CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF; + private boolean video_stabilization; + + private int getExifOrientation() { + int exif_orientation = ExifInterface.ORIENTATION_NORMAL; + switch( (rotation + 360) % 360 ) { + case 0: + exif_orientation = ExifInterface.ORIENTATION_NORMAL; + break; + case 90: + exif_orientation = isFrontFacing() ? + ExifInterface.ORIENTATION_ROTATE_270 : + ExifInterface.ORIENTATION_ROTATE_90; + break; + case 180: + exif_orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + exif_orientation = isFrontFacing() ? + ExifInterface.ORIENTATION_ROTATE_90 : + ExifInterface.ORIENTATION_ROTATE_270; + break; + default: + // leave exif_orientation unchanged + if( MyDebug.LOG ) + Log.e(TAG, "unexpected rotation: " + rotation); + break; + } + if( MyDebug.LOG ) { + Log.d(TAG, "rotation: " + rotation); + Log.d(TAG, "exif_orientation: " + exif_orientation); + } + return exif_orientation; + } + + private void setupBuilder(CaptureRequest.Builder builder, boolean is_still) { + //builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + //builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + //builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + //builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + //builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + + builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + setSceneMode(builder); + setColorEffect(builder); + setWhiteBalance(builder); + setAEMode(builder, is_still); + setCropRegion(builder); + setExposureCompensation(builder); + setFocusMode(builder); + setFocusDistance(builder); + setAutoExposureLock(builder); + setAFRegions(builder); + setAERegions(builder); + setFaceDetectMode(builder); + setRawMode(builder); + setVideoStabilization(builder); + + if( is_still ) { + if( location != null ) { + builder.set(CaptureRequest.JPEG_GPS_LOCATION, location); + } + builder.set(CaptureRequest.JPEG_ORIENTATION, rotation); + builder.set(CaptureRequest.JPEG_QUALITY, jpeg_quality); + } + } + + private boolean setSceneMode(CaptureRequest.Builder builder) { + if( MyDebug.LOG ) { + Log.d(TAG, "setSceneMode"); + Log.d(TAG, "builder: " + builder); + } + /*if( builder.get(CaptureRequest.CONTROL_SCENE_MODE) == null && scene_mode == CameraMetadata.CONTROL_SCENE_MODE_DISABLED ) { + // can leave off + } + else*/ if( builder.get(CaptureRequest.CONTROL_SCENE_MODE) == null || builder.get(CaptureRequest.CONTROL_SCENE_MODE) != scene_mode ) { + if( MyDebug.LOG ) + Log.d(TAG, "setting scene mode: " + scene_mode); + if( scene_mode == CameraMetadata.CONTROL_SCENE_MODE_DISABLED ) { + builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + } + else { + builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_USE_SCENE_MODE); + } + builder.set(CaptureRequest.CONTROL_SCENE_MODE, scene_mode); + return true; + } + return false; + } + + private boolean setColorEffect(CaptureRequest.Builder builder) { + /*if( builder.get(CaptureRequest.CONTROL_EFFECT_MODE) == null && color_effect == CameraMetadata.CONTROL_EFFECT_MODE_OFF ) { + // can leave off + } + else*/ if( builder.get(CaptureRequest.CONTROL_EFFECT_MODE) == null || builder.get(CaptureRequest.CONTROL_EFFECT_MODE) != color_effect ) { + if( MyDebug.LOG ) + Log.d(TAG, "setting color effect: " + color_effect); + builder.set(CaptureRequest.CONTROL_EFFECT_MODE, color_effect); + return true; + } + return false; + } + + private boolean setWhiteBalance(CaptureRequest.Builder builder) { + /*if( builder.get(CaptureRequest.CONTROL_AWB_MODE) == null && white_balance == CameraMetadata.CONTROL_AWB_MODE_AUTO ) { + // can leave off + } + else*/ if( builder.get(CaptureRequest.CONTROL_AWB_MODE) == null || builder.get(CaptureRequest.CONTROL_AWB_MODE) != white_balance ) { + if( MyDebug.LOG ) + Log.d(TAG, "setting white balance: " + white_balance); + builder.set(CaptureRequest.CONTROL_AWB_MODE, white_balance); + return true; + } + return false; + } + + private boolean setAEMode(CaptureRequest.Builder builder, boolean is_still) { + if( MyDebug.LOG ) + Log.d(TAG, "setAEMode"); + if( has_iso ) { + if( MyDebug.LOG ) { + Log.d(TAG, "manual mode"); + Log.d(TAG, "iso: " + iso); + Log.d(TAG, "exposure_time: " + exposure_time); + } + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF); + builder.set(CaptureRequest.SENSOR_SENSITIVITY, iso); + long actual_exposure_time = exposure_time; + if( !is_still ) { + // if this isn't for still capture, have a max exposure time of 1/12s + actual_exposure_time = Math.min(exposure_time, 1000000000L/12); + if( MyDebug.LOG ) + Log.d(TAG, "actually using exposure_time of: " + actual_exposure_time); + } + builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, actual_exposure_time); + // for now, flash is disabled when using manual iso - it seems to cause ISO level to jump to 100 on Nexus 6 when flash is turned on! + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + // set flash via CaptureRequest.FLASH + /*if( flash_value.equals("flash_off") ) { + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_auto") ) { + builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_on") ) { + builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_torch") ) { + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); + } + else if( flash_value.equals("flash_red_eye") ) { + builder.set(CaptureRequest.FLASH_MODE, is_still ? CameraMetadata.FLASH_MODE_SINGLE : CameraMetadata.FLASH_MODE_OFF); + }*/ + } + else { + if( MyDebug.LOG ) { + Log.d(TAG, "auto mode"); + Log.d(TAG, "flash_value: " + flash_value); + } + // prefer to set flash via the ae mode (otherwise get even worse results), except for torch which we can't + if( flash_value.equals("flash_off") ) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_auto") ) { + // note we set this even in fake flash mode (where we manually turn torch on and off to simulate flash) so we + // can read the FLASH_REQUIRED state to determine if flash is required + /*if( use_fake_precapture || CameraController2.this.want_expo_bracketing ) + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + else*/ + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_on") ) { + // see note above for "flash_auto" for why we set this even fake flash mode - arguably we don't need to know + // about FLASH_REQUIRED in flash_on mode, but we set it for consistency... + /*if( use_fake_precapture || CameraController2.this.want_expo_bracketing ) + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + else*/ + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_torch") ) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); + } + else if( flash_value.equals("flash_red_eye") ) { + // not supported for expo bracketing + if( CameraController2.this.want_expo_bracketing ) + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + else + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + else if( flash_value.equals("flash_frontscreen_auto") || flash_value.equals("flash_frontscreen_on") ) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + } + } + return true; + } + + private void setCropRegion(CaptureRequest.Builder builder) { + if( scalar_crop_region != null ) { + builder.set(CaptureRequest.SCALER_CROP_REGION, scalar_crop_region); + } + } + + private boolean setExposureCompensation(CaptureRequest.Builder builder) { + if( !has_ae_exposure_compensation ) + return false; + if( has_iso ) { + if( MyDebug.LOG ) + Log.d(TAG, "don't set exposure compensation in manual iso mode"); + return false; + } + if( builder.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION) == null || ae_exposure_compensation != builder.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION) ) { + if( MyDebug.LOG ) + Log.d(TAG, "change exposure to " + ae_exposure_compensation); + builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, ae_exposure_compensation); + return true; + } + return false; + } + + private void setFocusMode(CaptureRequest.Builder builder) { + if( has_af_mode ) { + if( MyDebug.LOG ) + Log.d(TAG, "change af mode to " + af_mode); + builder.set(CaptureRequest.CONTROL_AF_MODE, af_mode); + } + } + + private void setFocusDistance(CaptureRequest.Builder builder) { + if( MyDebug.LOG ) + Log.d(TAG, "change focus distance to " + focus_distance); + builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, focus_distance); + } + + private void setAutoExposureLock(CaptureRequest.Builder builder) { + builder.set(CaptureRequest.CONTROL_AE_LOCK, ae_lock); + } + + private void setAFRegions(CaptureRequest.Builder builder) { + if( af_regions != null && characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) > 0 ) { + builder.set(CaptureRequest.CONTROL_AF_REGIONS, af_regions); + } + } + + private void setAERegions(CaptureRequest.Builder builder) { + if( ae_regions != null && characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) > 0 ) { + builder.set(CaptureRequest.CONTROL_AE_REGIONS, ae_regions); + } + } + + private void setFaceDetectMode(CaptureRequest.Builder builder) { + if( has_face_detect_mode ) + builder.set(CaptureRequest.STATISTICS_FACE_DETECT_MODE, face_detect_mode); + else + builder.set(CaptureRequest.STATISTICS_FACE_DETECT_MODE, CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF); + } + + private void setRawMode(CaptureRequest.Builder builder) { + // DngCreator says "For best quality DNG files, it is strongly recommended that lens shading map output is enabled if supported" + // docs also say "ON is always supported on devices with the RAW capability", so we don't check for STATISTICS_LENS_SHADING_MAP_MODE_ON being available + if( want_raw ) { + builder.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE, CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON); + } + } + + private void setVideoStabilization(CaptureRequest.Builder builder) { + builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, video_stabilization ? CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON : CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF); + } + + // n.b., if we add more methods, remember to update setupBuilder() above! + } + + private class OnRawImageAvailableListener implements ImageReader.OnImageAvailableListener { + private CaptureResult capture_result; + private Image image; + + void setCaptureResult(CaptureResult capture_result) { + if( MyDebug.LOG ) + Log.d(TAG, "setCaptureResult()"); + synchronized( image_reader_lock ) { + /* synchronize, as we don't want to set the capture_result, at the same time that onImageAvailable() is called, as + * we'll end up calling processImage() both in onImageAvailable() and here. + */ + this.capture_result = capture_result; + if( image != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "can now process the image"); + processImage(); + } + } + } + + void clear() { + if( MyDebug.LOG ) + Log.d(TAG, "clear()"); + synchronized( image_reader_lock ) { + // synchronize just to be safe? + capture_result = null; + image = null; + } + } + + private void processImage() { + if( MyDebug.LOG ) + Log.d(TAG, "processImage()"); + if( capture_result == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "don't yet have still_capture_result"); + return; + } + if( image == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "don't have image?!"); + return; + } + if( MyDebug.LOG ) { + Log.d(TAG, "now have all info to process raw image"); + Log.d(TAG, "image timestamp: " + image.getTimestamp()); + } + DngCreator dngCreator = new DngCreator(characteristics, capture_result); + // set fields + dngCreator.setOrientation(camera_settings.getExifOrientation()); + if( camera_settings.location != null ) { + dngCreator.setLocation(camera_settings.location); + } + + pending_dngCreator = dngCreator; + pending_image = image; + + PictureCallback cb = raw_cb; + if( jpeg_cb == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "jpeg callback already done, so can go ahead with raw callback"); + takePendingRaw(); + if( MyDebug.LOG ) + Log.d(TAG, "all image callbacks now completed"); + cb.onCompleted(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "need to wait for jpeg callback"); + } + if( MyDebug.LOG ) + Log.d(TAG, "done processImage"); + } + + @Override + public void onImageAvailable(ImageReader reader) { + if( MyDebug.LOG ) + Log.d(TAG, "new still raw image available"); + if( raw_cb == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no picture callback available"); + return; + } + synchronized( image_reader_lock ) { + // see comment above in setCaptureResult() for why we sychonize + image = reader.acquireNextImage(); + processImage(); + } + if( MyDebug.LOG ) + Log.d(TAG, "done onImageAvailable"); + } + } + + private final CameraSettings camera_settings = new CameraSettings(); + private boolean push_repeating_request_when_torch_off = false; + private CaptureRequest push_repeating_request_when_torch_off_id = null; + /*private boolean push_set_ae_lock = false; + private CaptureRequest push_set_ae_lock_id = null;*/ + + private CaptureRequest fake_precapture_turn_on_torch_id = null; // the CaptureRequest used to turn on torch when starting the "fake" precapture + + @Override + public void onError() { + Log.e(TAG, "onError"); + if( camera != null ) { + onError(camera); + } + } + + private void onError(@NonNull CameraDevice cam) { + Log.e(TAG, "onError"); + boolean camera_already_opened = this.camera != null; + // need to set the camera to null first, as closing the camera may take some time, and we don't want any other operations to continue (if called from main thread) + this.camera = null; + if( MyDebug.LOG ) + Log.d(TAG, "onError: camera is now set to null"); + cam.close(); + if( MyDebug.LOG ) + Log.d(TAG, "onError: camera is now closed"); + + if( camera_already_opened ) { + // need to communicate the problem to the application + // n.b., as this is potentially serious error, we always log even if MyDebug.LOG is false + Log.e(TAG, "error occurred after camera was opened"); + camera_error_cb.onError(); + } + } + + /** Opens the camera device. + * @param context Application context. + * @param cameraId Which camera to open (must be between 0 and CameraControllerManager2.getNumberOfCameras()-1). + * @param preview_error_cb onError() will be called if the preview stops due to error. + * @param camera_error_cb onError() will be called if the camera closes due to serious error. No more calls to the CameraController2 object should be made (though a new one can be created, to try reopening the camera). + * @throws CameraControllerException if the camera device fails to open. + */ + public CameraController2(Context context, int cameraId, final ErrorCallback preview_error_cb, final ErrorCallback camera_error_cb) throws CameraControllerException { + super(cameraId); + if( MyDebug.LOG ) + Log.d(TAG, "create new CameraController2: " + cameraId); + + this.context = context; + this.preview_error_cb = preview_error_cb; + this.camera_error_cb = camera_error_cb; + + thread = new HandlerThread("CameraBackground"); + thread.start(); + handler = new Handler(thread.getLooper()); + + final CameraManager manager = (CameraManager)context.getSystemService(Context.CAMERA_SERVICE); + + class MyStateCallback extends CameraDevice.StateCallback { + boolean callback_done; // must sychronize on this and notifyAll when setting to true + boolean first_callback = true; // Google Camera says we may get multiple callbacks, but only the first indicates the status of the camera opening operation + @Override + public void onOpened(@NonNull CameraDevice cam) { + if( MyDebug.LOG ) + Log.d(TAG, "camera opened, first_callback? " + first_callback); + /*if( true ) // uncomment to test timeout code + return;*/ + if( first_callback ) { + first_callback = false; + + try { + // we should be able to get characteristics at any time, but Google Camera only does so when camera opened - so do so similarly to be safe + if( MyDebug.LOG ) + Log.d(TAG, "try to get camera characteristics"); + characteristics = manager.getCameraCharacteristics(cameraIdS); + if( MyDebug.LOG ) + Log.d(TAG, "successfully obtained camera characteristics"); + + CameraController2.this.camera = cam; + + // note, this won't start the preview yet, but we create the previewBuilder in order to start setting camera parameters + createPreviewRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to get camera characteristics"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + // don't throw CameraControllerException here - instead error is handled by setting callback_done to callback_done, and the fact that camera will still be null + } + + if( MyDebug.LOG ) + Log.d(TAG, "about to synchronize to say callback done"); + synchronized( open_camera_lock ) { + callback_done = true; + if( MyDebug.LOG ) + Log.d(TAG, "callback done, about to notify"); + open_camera_lock.notifyAll(); + if( MyDebug.LOG ) + Log.d(TAG, "callback done, notification done"); + } + } + } + + @Override + public void onClosed(@NonNull CameraDevice cam) { + if( MyDebug.LOG ) + Log.d(TAG, "camera closed, first_callback? " + first_callback); + // caller should ensure camera variables are set to null + if( first_callback ) { + first_callback = false; + } + } + + @Override + public void onDisconnected(@NonNull CameraDevice cam) { + if( MyDebug.LOG ) + Log.d(TAG, "camera disconnected, first_callback? " + first_callback); + if( first_callback ) { + first_callback = false; + // must call close() if disconnected before camera was opened + // need to set the camera to null first, as closing the camera may take some time, and we don't want any other operations to continue (if called from main thread) + CameraController2.this.camera = null; + if( MyDebug.LOG ) + Log.d(TAG, "onDisconnected: camera is now set to null"); + cam.close(); + if( MyDebug.LOG ) + Log.d(TAG, "onDisconnected: camera is now closed"); + if( MyDebug.LOG ) + Log.d(TAG, "about to synchronize to say callback done"); + synchronized( open_camera_lock ) { + callback_done = true; + if( MyDebug.LOG ) + Log.d(TAG, "callback done, about to notify"); + open_camera_lock.notifyAll(); + if( MyDebug.LOG ) + Log.d(TAG, "callback done, notification done"); + } + } + } + + @Override + public void onError(@NonNull CameraDevice cam, int error) { + // n.b., as this is potentially serious error, we always log even if MyDebug.LOG is false + Log.e(TAG, "camera error: " + error); + if( MyDebug.LOG ) { + Log.d(TAG, "received camera: " + cam); + Log.d(TAG, "actual camera: " + CameraController2.this.camera); + Log.d(TAG, "first_callback? " + first_callback); + } + if( first_callback ) { + first_callback = false; + } + CameraController2.this.onError(cam); + if( MyDebug.LOG ) + Log.d(TAG, "about to synchronize to say callback done"); + synchronized( open_camera_lock ) { + callback_done = true; + if( MyDebug.LOG ) + Log.d(TAG, "callback done, about to notify"); + open_camera_lock.notifyAll(); + if( MyDebug.LOG ) + Log.d(TAG, "callback done, notification done"); + } + } + } + final MyStateCallback myStateCallback = new MyStateCallback(); + + try { + if( MyDebug.LOG ) + Log.d(TAG, "get camera id list"); + this.cameraIdS = manager.getCameraIdList()[cameraId]; + if( MyDebug.LOG ) + Log.d(TAG, "about to open camera: " + cameraIdS); + manager.openCamera(cameraIdS, myStateCallback, handler); + if( MyDebug.LOG ) + Log.d(TAG, "open camera request complete"); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to open camera: CameraAccessException"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + throw new CameraControllerException(); + } + catch(UnsupportedOperationException e) { + // Google Camera catches UnsupportedOperationException + if( MyDebug.LOG ) { + Log.e(TAG, "failed to open camera: UnsupportedOperationException"); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + throw new CameraControllerException(); + } + catch(SecurityException e) { + // Google Camera catches SecurityException + if( MyDebug.LOG ) { + Log.e(TAG, "failed to open camera: SecurityException"); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + throw new CameraControllerException(); + } + + // set up a timeout - sometimes if the camera has got in a state where it can't be opened until after a reboot, we'll never even get a myStateCallback callback called + handler.postDelayed(new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "check if camera has opened in reasonable time"); + synchronized( open_camera_lock ) { + if( !myStateCallback.callback_done ) { + // n.b., as this is potentially serious error, we always log even if MyDebug.LOG is false + Log.e(TAG, "timeout waiting for camera callback"); + myStateCallback.first_callback = true; + myStateCallback.callback_done = true; + open_camera_lock.notifyAll(); + } + } + } + }, 10000); + + if( MyDebug.LOG ) + Log.d(TAG, "wait until camera opened..."); + // need to wait until camera is opened + synchronized( open_camera_lock ) { + while( !myStateCallback.callback_done ) { + try { + // release the lock, and wait until myStateCallback calls notifyAll() + open_camera_lock.wait(); + } + catch(InterruptedException e) { + if( MyDebug.LOG ) + Log.d(TAG, "interrupted while waiting until camera opened"); + e.printStackTrace(); + } + } + } + if( camera == null ) { + // n.b., as this is potentially serious error, we always log even if MyDebug.LOG is false + Log.e(TAG, "camera failed to open"); + throw new CameraControllerException(); + } + if( MyDebug.LOG ) + Log.d(TAG, "camera now opened: " + camera); + + /*{ + // test error handling + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "test camera error"); + myStateCallback.onError(camera, CameraDevice.StateCallback.ERROR_CAMERA_DEVICE); + } + }, 5000); + }*/ + + /*CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraIdS); + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size [] camera_picture_sizes = configs.getOutputSizes(ImageFormat.JPEG); + imageReader = ImageReader.newInstance(camera_picture_sizes[0].getWidth(), , ImageFormat.JPEG, 2);*/ + + // preload sounds to reduce latency - important so that START_VIDEO_RECORDING sound doesn't play after video has started (which means it'll be heard in the resultant video) + media_action_sound.load(MediaActionSound.START_VIDEO_RECORDING); + media_action_sound.load(MediaActionSound.STOP_VIDEO_RECORDING); + media_action_sound.load(MediaActionSound.SHUTTER_CLICK); + } + + @Override + public void release() { + if( MyDebug.LOG ) + Log.d(TAG, "release"); + if( thread != null ) { + thread.quitSafely(); + try { + thread.join(); + thread = null; + handler = null; + } + catch(InterruptedException e) { + e.printStackTrace(); + } + } + if( captureSession != null ) { + captureSession.close(); + captureSession = null; + } + previewBuilder = null; + if( camera != null ) { + camera.close(); + camera = null; + } + closePictureImageReader(); + /*if( previewImageReader != null ) { + previewImageReader.close(); + previewImageReader = null; + }*/ + } + + private void closePictureImageReader() { + if( MyDebug.LOG ) + Log.d(TAG, "closePictureImageReader()"); + if( imageReader != null ) { + imageReader.close(); + imageReader = null; + } + if( imageReaderRaw != null ) { + imageReaderRaw.close(); + imageReaderRaw = null; + onRawImageAvailableListener = null; + } + } + + private List convertFocusModesToValues(int [] supported_focus_modes_arr, float minimum_focus_distance) { + if( MyDebug.LOG ) + Log.d(TAG, "convertFocusModesToValues()"); + if( supported_focus_modes_arr.length == 0 ) + return null; + List supported_focus_modes = new ArrayList<>(); + for(Integer supported_focus_mode : supported_focus_modes_arr) + supported_focus_modes.add(supported_focus_mode); + List output_modes = new ArrayList<>(); + // also resort as well as converting + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) ) { + output_modes.add("focus_mode_auto"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_auto"); + } + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_MACRO) ) { + output_modes.add("focus_mode_macro"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_macro"); + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) ) { + output_modes.add("focus_mode_locked"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_locked"); + } + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_OFF) ) { + output_modes.add("focus_mode_infinity"); + if( minimum_focus_distance > 0.0f ) { + output_modes.add("focus_mode_manual2"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_manual2"); + } + } + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_EDOF) ) { + output_modes.add("focus_mode_edof"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_edof"); + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) ) { + output_modes.add("focus_mode_continuous_picture"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_continuous_picture"); + } + if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) ) { + output_modes.add("focus_mode_continuous_video"); + if( MyDebug.LOG ) + Log.d(TAG, " supports focus_mode_continuous_video"); + } + return output_modes; + } + + public String getAPI() { + return "Camera2 (Android L)"; + } + + @Override + public CameraFeatures getCameraFeatures() { + if( MyDebug.LOG ) + Log.d(TAG, "getCameraFeatures()"); + CameraFeatures camera_features = new CameraFeatures(); + if( MyDebug.LOG ) { + int hardware_level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + if( hardware_level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY ) + Log.d(TAG, "Hardware Level: LEGACY"); + else if( hardware_level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED ) + Log.d(TAG, "Hardware Level: LIMITED"); + else if( hardware_level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL ) + Log.d(TAG, "Hardware Level: FULL"); + else + Log.e(TAG, "Unknown Hardware Level!"); + } + + float max_zoom = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + camera_features.is_zoom_supported = max_zoom > 0.0f; + if( MyDebug.LOG ) + Log.d(TAG, "max_zoom: " + max_zoom); + if( camera_features.is_zoom_supported ) { + // set 20 steps per 2x factor + final int steps_per_2x_factor = 20; + //final double scale_factor = Math.pow(2.0, 1.0/(double)steps_per_2x_factor); + int n_steps =(int)( (steps_per_2x_factor * Math.log(max_zoom + 1.0e-11)) / Math.log(2.0)); + final double scale_factor = Math.pow(max_zoom, 1.0/(double)n_steps); + if( MyDebug.LOG ) { + Log.d(TAG, "n_steps: " + n_steps); + Log.d(TAG, "scale_factor: " + scale_factor); + } + camera_features.zoom_ratios = new ArrayList<>(); + camera_features.zoom_ratios.add(100); + double zoom = 1.0; + for(int i=0;i(); + for(android.util.Size camera_size : camera_picture_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "picture size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + camera_features.picture_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + } + + raw_size = null; + if( capabilities_raw ) { + android.util.Size [] raw_camera_picture_sizes = configs.getOutputSizes(ImageFormat.RAW_SENSOR); + if( raw_camera_picture_sizes == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "RAW not supported, failed to get RAW_SENSOR sizes"); + want_raw = false; // just in case it got set to true somehow + } + else { + for(android.util.Size size : raw_camera_picture_sizes) { + if( raw_size == null || size.getWidth()*size.getHeight() > raw_size.getWidth()*raw_size.getHeight() ) { + raw_size = size; + } + } + if( raw_size == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "RAW not supported, failed to find a raw size"); + want_raw = false; // just in case it got set to true somehow + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "raw supported, raw size: " + raw_size.getWidth() + " x " + raw_size.getHeight()); + camera_features.supports_raw = true; + } + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "RAW capability not supported"); + want_raw = false; // just in case it got set to true somehow + } + + android.util.Size [] camera_video_sizes = configs.getOutputSizes(MediaRecorder.class); + camera_features.video_sizes = new ArrayList<>(); + for(android.util.Size camera_size : camera_video_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "video size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + if( camera_size.getWidth() > 4096 || camera_size.getHeight() > 2160 ) + continue; // Nexus 6 returns these, even though not supported?! + camera_features.video_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + } + + if( capabilities_high_speed_video ) { + android.util.Size[] camera_video_sizes_high_speed = configs.getHighSpeedVideoSizes(); + camera_features.video_sizes_high_speed = new ArrayList<>(); + for (android.util.Size camera_size : camera_video_sizes_high_speed) { + if (MyDebug.LOG) + Log.d(TAG, "high speed video size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + if (camera_size.getWidth() > 4096 || camera_size.getHeight() > 2160) + continue; // just in case? see above + camera_features.video_sizes_high_speed.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + } + } + + android.util.Size [] camera_preview_sizes = configs.getOutputSizes(SurfaceTexture.class); + camera_features.preview_sizes = new ArrayList<>(); + Point display_size = new Point(); + Activity activity = (Activity)context; + { + Display display = activity.getWindowManager().getDefaultDisplay(); + display.getRealSize(display_size); + if( MyDebug.LOG ) + Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y); + } + for(android.util.Size camera_size : camera_preview_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "preview size: " + camera_size.getWidth() + " x " + camera_size.getHeight()); + if( camera_size.getWidth() > display_size.x || camera_size.getHeight() > display_size.y ) { + // Nexus 6 returns these, even though not supported?! (get green corruption lines if we allow these) + // Google Camera filters anything larger than height 1080, with a todo saying to use device's measurements + continue; + } + camera_features.preview_sizes.add(new CameraController.Size(camera_size.getWidth(), camera_size.getHeight())); + } + + if( characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ) { + camera_features.supported_flash_values = new ArrayList<>(); + camera_features.supported_flash_values.add("flash_off"); + camera_features.supported_flash_values.add("flash_auto"); + camera_features.supported_flash_values.add("flash_on"); + camera_features.supported_flash_values.add("flash_torch"); + if( !use_fake_precapture ) { + camera_features.supported_flash_values.add("flash_red_eye"); + } + } + else if( isFrontFacing() ) { + camera_features.supported_flash_values = new ArrayList<>(); + camera_features.supported_flash_values.add("flash_off"); + camera_features.supported_flash_values.add("flash_frontscreen_auto"); + camera_features.supported_flash_values.add("flash_frontscreen_on"); + } + + camera_features.minimum_focus_distance = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + if( MyDebug.LOG ) + Log.d(TAG, "minimum_focus_distance: " + camera_features.minimum_focus_distance); + int [] supported_focus_modes = characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); // Android format + camera_features.supported_focus_values = convertFocusModesToValues(supported_focus_modes, camera_features.minimum_focus_distance); // convert to our format (also resorts) + camera_features.max_num_focus_areas = characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + + camera_features.is_exposure_lock_supported = true; + + camera_features.is_video_stabilization_supported = true; + + Range iso_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); + if( iso_range != null ) { + camera_features.supports_iso_range = true; + camera_features.min_iso = iso_range.getLower(); + camera_features.max_iso = iso_range.getUpper(); + // we only expose exposure_time if iso_range is supported + Range exposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); + if( exposure_time_range != null ) { + camera_features.supports_exposure_time = true; + camera_features.supports_expo_bracketing = true; + camera_features.min_exposure_time = exposure_time_range.getLower(); + camera_features.max_exposure_time = exposure_time_range.getUpper(); + } + } + + Range exposure_range = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + camera_features.min_exposure = exposure_range.getLower(); + camera_features.max_exposure = exposure_range.getUpper(); + camera_features.exposure_step = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP).floatValue(); + + camera_features.can_disable_shutter_sound = true; + + { + // Calculate view angles + // Note this is an approximation (see http://stackoverflow.com/questions/39965408/what-is-the-android-camera2-api-equivalent-of-camera-parameters-gethorizontalvie ). + // Potentially we could do better, taking into account the aspect ratio of the current resolution. + // Note that we'd want to distinguish between the field of view of the preview versus the photo (or view) (for example, + // DrawPreview would want the preview's field of view). + // Also if we wanted to do this, we'd need to make sure that this was done after the caller had set the desired preview + // and photo/video resolutions. + SizeF physical_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); + float [] focal_lengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); + camera_features.view_angle_x = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getWidth(), (2.0 * focal_lengths[0]))); + camera_features.view_angle_y = (float)Math.toDegrees(2.0 * Math.atan2(physical_size.getHeight(), (2.0 * focal_lengths[0]))); + if( MyDebug.LOG ) { + Log.d(TAG, "view_angle_x: " + camera_features.view_angle_x); + Log.d(TAG, "view_angle_y: " + camera_features.view_angle_y); + } + } + + return camera_features; + } + + private String convertSceneMode(int value2) { + String value; + switch( value2 ) { + case CameraMetadata.CONTROL_SCENE_MODE_ACTION: + value = "action"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_BARCODE: + value = "barcode"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_BEACH: + value = "beach"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_CANDLELIGHT: + value = "candlelight"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_DISABLED: + value = "auto"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_FIREWORKS: + value = "fireworks"; + break; + // "hdr" no longer available in Camera2 + /*case CameraMetadata.CONTROL_SCENE_MODE_HIGH_SPEED_VIDEO: + // new for Camera2 + value = "high-speed-video"; + break;*/ + case CameraMetadata.CONTROL_SCENE_MODE_LANDSCAPE: + value = "landscape"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_NIGHT: + value = "night"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_NIGHT_PORTRAIT: + value = "night-portrait"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_PARTY: + value = "party"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_PORTRAIT: + value = "portrait"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_SNOW: + value = "snow"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_SPORTS: + value = "sports"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_STEADYPHOTO: + value = "steadyphoto"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_SUNSET: + value = "sunset"; + break; + case CameraMetadata.CONTROL_SCENE_MODE_THEATRE: + value = "theatre"; + break; + default: + if( MyDebug.LOG ) + Log.d(TAG, "unknown scene mode: " + value2); + value = null; + break; + } + return value; + } + + @Override + public SupportedValues setSceneMode(String value) { + if( MyDebug.LOG ) + Log.d(TAG, "setSceneMode: " + value); + // we convert to/from strings to be compatible with original Android Camera API + String default_value = getDefaultSceneMode(); + int [] values2 = characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES); + boolean has_disabled = false; + List values = new ArrayList<>(); + for(int value2 : values2) { + if( value2 == CameraMetadata.CONTROL_SCENE_MODE_DISABLED ) + has_disabled = true; + String this_value = convertSceneMode(value2); + if( this_value != null ) { + values.add(this_value); + } + } + if( !has_disabled ) { + values.add(0, "auto"); + } + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + int selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_DISABLED; + if( supported_values.selected_value.equals("action") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_ACTION; + } + else if( supported_values.selected_value.equals("barcode") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_BARCODE; + } + else if( supported_values.selected_value.equals("beach") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_BEACH; + } + else if( supported_values.selected_value.equals("candlelight") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_CANDLELIGHT; + } + else if( supported_values.selected_value.equals("auto") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_DISABLED; + } + else if( supported_values.selected_value.equals("fireworks") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_FIREWORKS; + } + // "hdr" no longer available in Camera2 + else if( supported_values.selected_value.equals("landscape") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_LANDSCAPE; + } + else if( supported_values.selected_value.equals("night") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_NIGHT; + } + else if( supported_values.selected_value.equals("night-portrait") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_NIGHT_PORTRAIT; + } + else if( supported_values.selected_value.equals("party") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_PARTY; + } + else if( supported_values.selected_value.equals("portrait") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_PORTRAIT; + } + else if( supported_values.selected_value.equals("snow") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_SNOW; + } + else if( supported_values.selected_value.equals("sports") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_SPORTS; + } + else if( supported_values.selected_value.equals("steadyphoto") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_STEADYPHOTO; + } + else if( supported_values.selected_value.equals("sunset") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_SUNSET; + } + else if( supported_values.selected_value.equals("theatre") ) { + selected_value2 = CameraMetadata.CONTROL_SCENE_MODE_THEATRE; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "unknown selected_value: " + supported_values.selected_value); + } + + camera_settings.scene_mode = selected_value2; + if( camera_settings.setSceneMode(previewBuilder) ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set scene mode"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + } + return supported_values; + } + + @Override + public String getSceneMode() { + if( previewBuilder.get(CaptureRequest.CONTROL_SCENE_MODE) == null ) + return null; + int value2 = previewBuilder.get(CaptureRequest.CONTROL_SCENE_MODE); + return convertSceneMode(value2); + } + + private String convertColorEffect(int value2) { + String value; + switch( value2 ) { + case CameraMetadata.CONTROL_EFFECT_MODE_AQUA: + value = "aqua"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_BLACKBOARD: + value = "blackboard"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_MONO: + value = "mono"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_NEGATIVE: + value = "negative"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_OFF: + value = "none"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_POSTERIZE: + value = "posterize"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_SEPIA: + value = "sepia"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_SOLARIZE: + value = "solarize"; + break; + case CameraMetadata.CONTROL_EFFECT_MODE_WHITEBOARD: + value = "whiteboard"; + break; + default: + if( MyDebug.LOG ) + Log.d(TAG, "unknown effect mode: " + value2); + value = null; + break; + } + return value; + } + + @Override + public SupportedValues setColorEffect(String value) { + if( MyDebug.LOG ) + Log.d(TAG, "setColorEffect: " + value); + // we convert to/from strings to be compatible with original Android Camera API + String default_value = getDefaultColorEffect(); + int [] values2 = characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS); + List values = new ArrayList<>(); + for(int value2 : values2) { + String this_value = convertColorEffect(value2); + if( this_value != null ) { + values.add(this_value); + } + } + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + int selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_OFF; + if( supported_values.selected_value.equals("aqua") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_AQUA; + } + else if( supported_values.selected_value.equals("blackboard") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_BLACKBOARD; + } + else if( supported_values.selected_value.equals("mono") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_MONO; + } + else if( supported_values.selected_value.equals("negative") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_NEGATIVE; + } + else if( supported_values.selected_value.equals("none") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_OFF; + } + else if( supported_values.selected_value.equals("posterize") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_POSTERIZE; + } + else if( supported_values.selected_value.equals("sepia") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_SEPIA; + } + else if( supported_values.selected_value.equals("solarize") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_SOLARIZE; + } + else if( supported_values.selected_value.equals("whiteboard") ) { + selected_value2 = CameraMetadata.CONTROL_EFFECT_MODE_WHITEBOARD; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "unknown selected_value: " + supported_values.selected_value); + } + + camera_settings.color_effect = selected_value2; + if( camera_settings.setColorEffect(previewBuilder) ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set color effect"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + } + return supported_values; + } + + @Override + public String getColorEffect() { + if( previewBuilder.get(CaptureRequest.CONTROL_EFFECT_MODE) == null ) + return null; + int value2 = previewBuilder.get(CaptureRequest.CONTROL_EFFECT_MODE); + return convertColorEffect(value2); + } + + private String convertWhiteBalance(int value2) { + String value; + switch( value2 ) { + case CameraMetadata.CONTROL_AWB_MODE_AUTO: + value = "auto"; + break; + case CameraMetadata.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT: + value = "cloudy-daylight"; + break; + case CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT: + value = "daylight"; + break; + case CameraMetadata.CONTROL_AWB_MODE_FLUORESCENT: + value = "fluorescent"; + break; + case CameraMetadata.CONTROL_AWB_MODE_INCANDESCENT: + value = "incandescent"; + break; + case CameraMetadata.CONTROL_AWB_MODE_SHADE: + value = "shade"; + break; + case CameraMetadata.CONTROL_AWB_MODE_TWILIGHT: + value = "twilight"; + break; + case CameraMetadata.CONTROL_AWB_MODE_WARM_FLUORESCENT: + value = "warm-fluorescent"; + break; + default: + if( MyDebug.LOG ) + Log.d(TAG, "unknown white balance: " + value2); + value = null; + break; + } + return value; + } + + @Override + public SupportedValues setWhiteBalance(String value) { + if( MyDebug.LOG ) + Log.d(TAG, "setWhiteBalance: " + value); + // we convert to/from strings to be compatible with original Android Camera API + String default_value = getDefaultWhiteBalance(); + int [] values2 = characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); + List values = new ArrayList<>(); + for(int value2 : values2) { + String this_value = convertWhiteBalance(value2); + if( this_value != null ) { + values.add(this_value); + } + } + SupportedValues supported_values = checkModeIsSupported(values, value, default_value); + if( supported_values != null ) { + int selected_value2 = CameraMetadata.CONTROL_AWB_MODE_AUTO; + if( supported_values.selected_value.equals("auto") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_AUTO; + } + else if( supported_values.selected_value.equals("cloudy-daylight") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT; + } + else if( supported_values.selected_value.equals("daylight") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT; + } + else if( supported_values.selected_value.equals("fluorescent") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_FLUORESCENT; + } + else if( supported_values.selected_value.equals("incandescent") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_INCANDESCENT; + } + else if( supported_values.selected_value.equals("shade") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_SHADE; + } + else if( supported_values.selected_value.equals("twilight") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_TWILIGHT; + } + else if( supported_values.selected_value.equals("warm-fluorescent") ) { + selected_value2 = CameraMetadata.CONTROL_AWB_MODE_WARM_FLUORESCENT; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "unknown selected_value: " + supported_values.selected_value); + } + + camera_settings.white_balance = selected_value2; + if( camera_settings.setWhiteBalance(previewBuilder) ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set white balance"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + } + return supported_values; + } + + @Override + public String getWhiteBalance() { + if( previewBuilder.get(CaptureRequest.CONTROL_AWB_MODE) == null ) + return null; + int value2 = previewBuilder.get(CaptureRequest.CONTROL_AWB_MODE); + return convertWhiteBalance(value2); + } + + @Override + public SupportedValues setISO(String value) { + // not supported for CameraController2 + Log.e(TAG, "setISO(String value) not supported for CameraController2"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + + @Override + public String getISOKey() { + return ""; + } + + @Override + public void setManualISO(boolean manual_iso, int iso) { + if( MyDebug.LOG ) + Log.d(TAG, "setManualISO" + manual_iso); + try { + if( manual_iso ) { + if( MyDebug.LOG ) + Log.d(TAG, "switch to iso: " + iso); + Range iso_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); + if( iso_range == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "iso not supported"); + return; + } + if( MyDebug.LOG ) + Log.d(TAG, "iso range from " + iso_range.getLower() + " to " + iso_range.getUpper()); + + camera_settings.has_iso = true; + iso = Math.max(iso, iso_range.getLower()); + iso = Math.min(iso, iso_range.getUpper()); + camera_settings.iso = iso; + } + else { + camera_settings.has_iso = false; + camera_settings.iso = 0; + } + + if( camera_settings.setAEMode(previewBuilder, false) ) { + setRepeatingRequest(); + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set ISO"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + @Override + public boolean isManualISO() { + return camera_settings.has_iso; + } + + @Override + // Returns whether ISO was modified + // N.B., use setManualISO() to switch between auto and manual mode + public boolean setISO(int iso) { + if( MyDebug.LOG ) + Log.d(TAG, "setISO: " + iso); + if( camera_settings.iso == iso ) { + if( MyDebug.LOG ) + Log.d(TAG, "already set"); + return false; + } + try { + camera_settings.iso = iso; + if( camera_settings.setAEMode(previewBuilder, false) ) { + setRepeatingRequest(); + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set ISO"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + return true; + } + + @Override + public int getISO() { + return camera_settings.iso; + } + + @Override + public long getExposureTime() { + return camera_settings.exposure_time; + } + + @Override + // Returns whether exposure time was modified + // N.B., use setISO(String) to switch between auto and manual mode + public boolean setExposureTime(long exposure_time) { + if( MyDebug.LOG ) { + Log.d(TAG, "setExposureTime: " + exposure_time); + Log.d(TAG, "current exposure time: " + camera_settings.exposure_time); + } + if( camera_settings.exposure_time == exposure_time ) { + if( MyDebug.LOG ) + Log.d(TAG, "already set"); + return false; + } + try { + camera_settings.exposure_time = exposure_time; + if( camera_settings.setAEMode(previewBuilder, false) ) { + setRepeatingRequest(); + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set exposure time"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + return true; + } + + @Override + public Size getPictureSize() { + return new Size(picture_width, picture_height); + } + + @Override + public void setPictureSize(int width, int height) { + if( MyDebug.LOG ) + Log.d(TAG, "setPictureSize: " + width + " x " + height); + if( camera == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "no camera"); + return; + } + if( captureSession != null ) { + // can only call this when captureSession not created - as the surface of the imageReader we create has to match the surface we pass to the captureSession + if( MyDebug.LOG ) + Log.e(TAG, "can't set picture size when captureSession running!"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.picture_width = width; + this.picture_height = height; + } + + @Override + public void setRaw(boolean want_raw) { + if( MyDebug.LOG ) + Log.d(TAG, "setRaw: " + want_raw); + if( camera == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "no camera"); + return; + } + if( this.want_raw == want_raw ) { + return; + } + if( want_raw && this.raw_size == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "can't set raw when raw not supported"); + return; + } + if( captureSession != null ) { + // can only call this when captureSession not created - as it affects how we create the imageReader + if( MyDebug.LOG ) + Log.e(TAG, "can't set raw when captureSession running!"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.want_raw = want_raw; + } + + @Override + public void setExpoBracketing(boolean want_expo_bracketing) { + if( MyDebug.LOG ) + Log.d(TAG, "setExpoBracketing: " + want_expo_bracketing); + if( camera == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "no camera"); + return; + } + if( this.want_expo_bracketing == want_expo_bracketing ) { + return; + } + if( captureSession != null ) { + // can only call this when captureSession not created - as it affects how we create the imageReader + if( MyDebug.LOG ) + Log.e(TAG, "can't set hdr when captureSession running!"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.want_expo_bracketing = want_expo_bracketing; + updateUseFakePrecaptureMode(camera_settings.flash_value); + camera_settings.setAEMode(previewBuilder, false); // need to set the ae mode, as flash is disabled for HDR mode + } + + @Override + public void setExpoBracketingNImages(int n_images) { + if( MyDebug.LOG ) + Log.d(TAG, "setExpoBracketingNImages: " + n_images); + if( n_images <= 1 || (n_images % 2) == 0 ) { + if( MyDebug.LOG ) + Log.e(TAG, "n_images should be an odd number greater than 1"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.expo_bracketing_n_images = n_images; + } + + @Override + public void setExpoBracketingStops(double stops) { + if( MyDebug.LOG ) + Log.d(TAG, "setExpoBracketingStops: " + stops); + if( stops <= 0.0 ) { + if( MyDebug.LOG ) + Log.e(TAG, "stops should be positive"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.expo_bracketing_stops = stops; + } + + @Override + public void setUseExpoFastBurst(boolean use_expo_fast_burst) { + if( MyDebug.LOG ) + Log.d(TAG, "setUseExpoFastBurst: " + use_expo_fast_burst); + this.use_expo_fast_burst = use_expo_fast_burst; + } + + @Override + public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { + if (MyDebug.LOG) + Log.d(TAG, "clearCaptureExposureScaleStops"); + this.optimise_ae_for_dro = optimise_ae_for_dro; + } + + @Override + public void setUseCamera2FakeFlash(boolean use_fake_precapture) { + if( MyDebug.LOG ) + Log.d(TAG, "setUseCamera2FakeFlash: " + use_fake_precapture); + if( camera == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "no camera"); + return; + } + if( this.use_fake_precapture == use_fake_precapture ) { + return; + } + this.use_fake_precapture = use_fake_precapture; + this.use_fake_precapture_mode = use_fake_precapture; + // no need to call updateUseFakePrecaptureMode(), as this method should only be called after first creating camera controller + } + + @Override + public boolean getUseCamera2FakeFlash() { + return this.use_fake_precapture; + } + + private void createPictureImageReader() { + if( MyDebug.LOG ) + Log.d(TAG, "createPictureImageReader"); + if( captureSession != null ) { + // can only call this when captureSession not created - as the surface of the imageReader we create has to match the surface we pass to the captureSession + if( MyDebug.LOG ) + Log.e(TAG, "can't create picture image reader when captureSession running!"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + closePictureImageReader(); + if( picture_width == 0 || picture_height == 0 ) { + if( MyDebug.LOG ) + Log.e(TAG, "application needs to call setPictureSize()"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + imageReader = ImageReader.newInstance(picture_width, picture_height, ImageFormat.JPEG, 2); + if( MyDebug.LOG ) { + Log.d(TAG, "created new imageReader: " + imageReader.toString()); + Log.d(TAG, "imageReader surface: " + imageReader.getSurface().toString()); + } + imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + if( MyDebug.LOG ) + Log.d(TAG, "new still image available"); + if( jpeg_cb == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no picture callback available"); + return; + } + synchronized( image_reader_lock ) { + /* Whilst in theory the two setOnImageAvailableListener methods (for JPEG and RAW) seem to be called separately, I don't know if this is always true; + * also, we may process the RAW image when the capture result is available (see + * OnRawImageAvailableListener.setCaptureResult()), which may be in a separate thread. + */ + Image image = reader.acquireNextImage(); + if( MyDebug.LOG ) + Log.d(TAG, "image timestamp: " + image.getTimestamp()); + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte [] bytes = new byte[buffer.remaining()]; + if( MyDebug.LOG ) + Log.d(TAG, "read " + bytes.length + " bytes"); + buffer.get(bytes); + image.close(); + if( want_expo_bracketing && n_burst > 1 ) { + pending_burst_images.add(bytes); + if( pending_burst_images.size() >= n_burst ) { // shouldn't ever be greater, but just in case + if( MyDebug.LOG ) + Log.d(TAG, "all burst images available"); + if( pending_burst_images.size() > n_burst ) { + Log.e(TAG, "pending_burst_images size " + pending_burst_images.size() + " is greater than n_burst " + n_burst); + } + // need to set jpeg_cb etc to null before calling onCompleted, as that may reenter CameraController to take another photo (if in burst mode) - see testTakePhotoBurst() + PictureCallback cb = jpeg_cb; + jpeg_cb = null; + // take a copy, so that we can clear pending_burst_images + List images = new ArrayList<>(pending_burst_images); + cb.onBurstPictureTaken(images); + pending_burst_images.clear(); + cb.onCompleted(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "number of burst images is now: " + pending_burst_images.size()); + if( burst_capture_requests != null ) { + if( MyDebug.LOG ) { + Log.d(TAG, "need to execute the next capture"); + Log.d(TAG, "time since start: " + (System.currentTimeMillis() - burst_start_ms)); + } + try { + captureSession.capture(burst_capture_requests.get(pending_burst_images.size()), previewCaptureCallback, handler); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to take next burst"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + jpeg_cb = null; + if( take_picture_error_cb != null ) { + take_picture_error_cb.onError(); + take_picture_error_cb = null; + } + } + } + } + } + else { + // need to set jpeg_cb etc to null before calling onCompleted, as that may reenter CameraController to take another photo (if in burst mode) - see testTakePhotoBurst() + PictureCallback cb = jpeg_cb; + jpeg_cb = null; + cb.onPictureTaken(bytes); + if( raw_cb == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "all image callbacks now completed"); + cb.onCompleted(); + } + else if( pending_dngCreator != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "can now call pending raw callback"); + takePendingRaw(); + if( MyDebug.LOG ) + Log.d(TAG, "all image callbacks now completed"); + cb.onCompleted(); + } + } + } + if( MyDebug.LOG ) + Log.d(TAG, "done onImageAvailable"); + } + }, null); + if( want_raw && raw_size != null ) { + imageReaderRaw = ImageReader.newInstance(raw_size.getWidth(), raw_size.getHeight(), ImageFormat.RAW_SENSOR, 2); + if( MyDebug.LOG ) { + Log.d(TAG, "created new imageReaderRaw: " + imageReaderRaw.toString()); + Log.d(TAG, "imageReaderRaw surface: " + imageReaderRaw.getSurface().toString()); + } + imageReaderRaw.setOnImageAvailableListener(onRawImageAvailableListener = new OnRawImageAvailableListener(), null); + } + } + + private void clearPending() { + if( MyDebug.LOG ) + Log.d(TAG, "clearPending"); + pending_burst_images.clear(); + pending_dngCreator = null; + pending_image = null; + if( onRawImageAvailableListener != null ) { + onRawImageAvailableListener.clear(); + } + burst_capture_requests = null; + n_burst = 0; + burst_start_ms = 0; + } + + private void takePendingRaw() { + if( MyDebug.LOG ) + Log.d(TAG, "takePendingRaw"); + if( pending_dngCreator != null ) { + PictureCallback cb = raw_cb; + raw_cb = null; + cb.onRawPictureTaken(pending_dngCreator, pending_image); + // image and dngCreator should be closed by the application (we don't do it here, so that applications can keep hold of the data, e.g., in a queue for background processing) + pending_dngCreator = null; + pending_image = null; + if( onRawImageAvailableListener != null ) { + onRawImageAvailableListener.clear(); + } + } + } + + @Override + public Size getPreviewSize() { + return new Size(preview_width, preview_height); + } + + @Override + public void setPreviewSize(int width, int height) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewSize: " + width + " , " + height); + /*if( texture != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "set size of preview texture"); + texture.setDefaultBufferSize(width, height); + }*/ + preview_width = width; + preview_height = height; + /*if( previewImageReader != null ) { + previewImageReader.close(); + } + previewImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2); + */ + } + + @Override + public void setVideoStabilization(boolean enabled) { + camera_settings.video_stabilization = enabled; + camera_settings.setVideoStabilization(previewBuilder); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set video stabilization"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + @Override + public boolean getVideoStabilization() { + return camera_settings.video_stabilization; + } + + @Override + public int getJpegQuality() { + return this.camera_settings.jpeg_quality; + } + + @Override + public void setJpegQuality(int quality) { + if( quality < 0 || quality > 100 ) { + if( MyDebug.LOG ) + Log.e(TAG, "invalid jpeg quality" + quality); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.camera_settings.jpeg_quality = (byte)quality; + } + + @Override + public int getZoom() { + return this.current_zoom_value; + } + + @Override + public void setZoom(int value) { + if( zoom_ratios == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "zoom not supported"); + return; + } + if( value < 0 || value > zoom_ratios.size() ) { + if( MyDebug.LOG ) + Log.e(TAG, "invalid zoom value" + value); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + float zoom = zoom_ratios.get(value)/100.0f; + Rect sensor_rect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + int left = sensor_rect.width()/2; + int right = left; + int top = sensor_rect.height()/2; + int bottom = top; + int hwidth = (int)(sensor_rect.width() / (2.0*zoom)); + int hheight = (int)(sensor_rect.height() / (2.0*zoom)); + left -= hwidth; + right += hwidth; + top -= hheight; + bottom += hheight; + if( MyDebug.LOG ) { + Log.d(TAG, "zoom: " + zoom); + Log.d(TAG, "hwidth: " + hwidth); + Log.d(TAG, "hheight: " + hheight); + Log.d(TAG, "sensor_rect left: " + sensor_rect.left); + Log.d(TAG, "sensor_rect top: " + sensor_rect.top); + Log.d(TAG, "sensor_rect right: " + sensor_rect.right); + Log.d(TAG, "sensor_rect bottom: " + sensor_rect.bottom); + Log.d(TAG, "left: " + left); + Log.d(TAG, "top: " + top); + Log.d(TAG, "right: " + right); + Log.d(TAG, "bottom: " + bottom); + /*Rect current_rect = previewBuilder.get(CaptureRequest.SCALER_CROP_REGION); + Log.d(TAG, "current_rect left: " + current_rect.left); + Log.d(TAG, "current_rect top: " + current_rect.top); + Log.d(TAG, "current_rect right: " + current_rect.right); + Log.d(TAG, "current_rect bottom: " + current_rect.bottom);*/ + } + camera_settings.scalar_crop_region = new Rect(left, top, right, bottom); + camera_settings.setCropRegion(previewBuilder); + this.current_zoom_value = value; + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set zoom"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + @Override + public int getExposureCompensation() { + if( previewBuilder.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION) == null ) + return 0; + return previewBuilder.get(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION); + } + + @Override + // Returns whether exposure was modified + public boolean setExposureCompensation(int new_exposure) { + camera_settings.has_ae_exposure_compensation = true; + camera_settings.ae_exposure_compensation = new_exposure; + if( camera_settings.setExposureCompensation(previewBuilder) ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set exposure compensation"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + return true; + } + return false; + } + + @Override + public void setPreviewFpsRange(int min, int max) { + // TODO Auto-generated method stub + + } + + @Override + public List getSupportedPreviewFpsRange() { + // TODO Auto-generated method stub + return null; + } + + @Override + // note, responsibility of callers to check that this is within the valid min/max range + public long getDefaultExposureTime() { + return 1000000000L/30; + } + + @Override + public void setFocusValue(String focus_value) { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusValue: " + focus_value); + int focus_mode; + if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_locked") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_AUTO; + } + else if( focus_value.equals("focus_mode_infinity") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_OFF; + camera_settings.focus_distance = 0.0f; + } + else if( focus_value.equals("focus_mode_manual2") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_OFF; + camera_settings.focus_distance = camera_settings.focus_distance_manual; + } + else if( focus_value.equals("focus_mode_macro") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_MACRO; + } + else if( focus_value.equals("focus_mode_edof") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_EDOF; + } + else if( focus_value.equals("focus_mode_continuous_picture") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE; + } + else if( focus_value.equals("focus_mode_continuous_video") ) { + focus_mode = CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusValue() received unknown focus value " + focus_value); + return; + } + camera_settings.has_af_mode = true; + camera_settings.af_mode = focus_mode; + camera_settings.setFocusMode(previewBuilder); + camera_settings.setFocusDistance(previewBuilder); // also need to set distance, in case changed between infinity, manual or other modes + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set focus mode"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + private String convertFocusModeToValue(int focus_mode) { + if( MyDebug.LOG ) + Log.d(TAG, "convertFocusModeToValue: " + focus_mode); + String focus_value = ""; + if( focus_mode == CaptureRequest.CONTROL_AF_MODE_AUTO ) { + focus_value = "focus_mode_auto"; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_MACRO ) { + focus_value = "focus_mode_macro"; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_EDOF ) { + focus_value = "focus_mode_edof"; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) { + focus_value = "focus_mode_continuous_picture"; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO ) { + focus_value = "focus_mode_continuous_video"; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_OFF ) { + focus_value = "focus_mode_manual2"; // n.b., could be infinity + } + return focus_value; + } + + @Override + public String getFocusValue() { + int focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE) != null ? + previewBuilder.get(CaptureRequest.CONTROL_AF_MODE) : CaptureRequest.CONTROL_AF_MODE_AUTO; + return convertFocusModeToValue(focus_mode); + } + + @Override + public float getFocusDistance() { + return camera_settings.focus_distance; + } + + @Override + public boolean setFocusDistance(float focus_distance) { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusDistance: " + focus_distance); + if( camera_settings.focus_distance == focus_distance ) { + if( MyDebug.LOG ) + Log.d(TAG, "already set"); + return false; + } + camera_settings.focus_distance = focus_distance; + camera_settings.focus_distance_manual = focus_distance; + camera_settings.setFocusDistance(previewBuilder); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set focus distance"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + return true; + } + + /** Decides whether we should be using fake precapture mode. + */ + private void updateUseFakePrecaptureMode(String flash_value) { + if( MyDebug.LOG ) + Log.d(TAG, "useFakePrecaptureMode: " + flash_value); + boolean frontscreen_flash = flash_value.equals("flash_frontscreen_auto") || flash_value.equals("flash_frontscreen_on"); + if( frontscreen_flash ) { + use_fake_precapture_mode = true; + } + else if( this.want_expo_bracketing ) + use_fake_precapture_mode = true; + else { + use_fake_precapture_mode = use_fake_precapture; + } + if( MyDebug.LOG ) + Log.d(TAG, "use_fake_precapture_mode set to: " + use_fake_precapture_mode); + } + + @Override + public void setFlashValue(String flash_value) { + if( MyDebug.LOG ) + Log.d(TAG, "setFlashValue: " + flash_value); + if( camera_settings.flash_value.equals(flash_value) ) { + if( MyDebug.LOG ) + Log.d(TAG, "flash value already set"); + return; + } + + try { + updateUseFakePrecaptureMode(flash_value); + + if( camera_settings.flash_value.equals("flash_torch") && !flash_value.equals("flash_off") ) { + // hack - if switching to something other than flash_off, we first need to turn torch off, otherwise torch remains on (at least on Nexus 6) + camera_settings.flash_value = "flash_off"; + camera_settings.setAEMode(previewBuilder, false); + CaptureRequest request = previewBuilder.build(); + + // need to wait until torch actually turned off + camera_settings.flash_value = flash_value; + camera_settings.setAEMode(previewBuilder, false); + push_repeating_request_when_torch_off = true; + push_repeating_request_when_torch_off_id = request; + + setRepeatingRequest(request); + } + else { + camera_settings.flash_value = flash_value; + if( camera_settings.setAEMode(previewBuilder, false) ) { + setRepeatingRequest(); + } + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set flash mode"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + @Override + public String getFlashValue() { + // returns "" if flash isn't supported + if( !characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ) { + return ""; + } + return camera_settings.flash_value; + } + + @Override + public void setRecordingHint(boolean hint) { + // not relevant for CameraController2 + } + + @Override + public void setAutoExposureLock(boolean enabled) { + camera_settings.ae_lock = enabled; + camera_settings.setAutoExposureLock(previewBuilder); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set auto exposure lock"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + @Override + public boolean getAutoExposureLock() { + if( previewBuilder.get(CaptureRequest.CONTROL_AE_LOCK) == null ) + return false; + return previewBuilder.get(CaptureRequest.CONTROL_AE_LOCK); + } + + @Override + public void setRotation(int rotation) { + this.camera_settings.rotation = rotation; + } + + @Override + public void setLocationInfo(Location location) { + if( MyDebug.LOG ) + Log.d(TAG, "setLocationInfo: " + location.getLongitude() + " , " + location.getLatitude()); + this.camera_settings.location = location; + } + + @Override + public void removeLocationInfo() { + this.camera_settings.location = null; + } + + @Override + public void enableShutterSound(boolean enabled) { + this.sounds_enabled = enabled; + } + + /** Returns the viewable rect - this is crop region if available. + * We need this as callers will pass in (or expect returned) CameraController.Area values that + * are relative to the current view (i.e., taking zoom into account) (the old Camera API in + * CameraController1 always works in terms of the current view, whilst Camera2 works in terms + * of the full view always). Similarly for the rect field in CameraController.Face. + */ + private Rect getViewableRect() { + if( previewBuilder != null ) { + Rect crop_rect = previewBuilder.get(CaptureRequest.SCALER_CROP_REGION); + if( crop_rect != null ) { + return crop_rect; + } + } + Rect sensor_rect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + sensor_rect.right -= sensor_rect.left; + sensor_rect.left = 0; + sensor_rect.bottom -= sensor_rect.top; + sensor_rect.top = 0; + return sensor_rect; + } + + private Rect convertRectToCamera2(Rect crop_rect, Rect rect) { + // CameraController.Area is always [-1000, -1000] to [1000, 1000] for the viewable region + // but for CameraController2, we must convert to be relative to the crop region + double left_f = (rect.left+1000)/2000.0; + double top_f = (rect.top+1000)/2000.0; + double right_f = (rect.right+1000)/2000.0; + double bottom_f = (rect.bottom+1000)/2000.0; + int left = (int)(crop_rect.left + left_f * (crop_rect.width()-1)); + int right = (int)(crop_rect.left + right_f * (crop_rect.width()-1)); + int top = (int)(crop_rect.top + top_f * (crop_rect.height()-1)); + int bottom = (int)(crop_rect.top + bottom_f * (crop_rect.height()-1)); + left = Math.max(left, crop_rect.left); + right = Math.max(right, crop_rect.left); + top = Math.max(top, crop_rect.top); + bottom = Math.max(bottom, crop_rect.top); + left = Math.min(left, crop_rect.right); + right = Math.min(right, crop_rect.right); + top = Math.min(top, crop_rect.bottom); + bottom = Math.min(bottom, crop_rect.bottom); + + return new Rect(left, top, right, bottom); + } + + private MeteringRectangle convertAreaToMeteringRectangle(Rect sensor_rect, Area area) { + Rect camera2_rect = convertRectToCamera2(sensor_rect, area.rect); + return new MeteringRectangle(camera2_rect, area.weight); + } + + private Rect convertRectFromCamera2(Rect crop_rect, Rect camera2_rect) { + // inverse of convertRectToCamera2() + double left_f = (camera2_rect.left-crop_rect.left)/(double)(crop_rect.width()-1); + double top_f = (camera2_rect.top-crop_rect.top)/(double)(crop_rect.height()-1); + double right_f = (camera2_rect.right-crop_rect.left)/(double)(crop_rect.width()-1); + double bottom_f = (camera2_rect.bottom-crop_rect.top)/(double)(crop_rect.height()-1); + int left = (int)(left_f * 2000) - 1000; + int right = (int)(right_f * 2000) - 1000; + int top = (int)(top_f * 2000) - 1000; + int bottom = (int)(bottom_f * 2000) - 1000; + + left = Math.max(left, -1000); + right = Math.max(right, -1000); + top = Math.max(top, -1000); + bottom = Math.max(bottom, -1000); + left = Math.min(left, 1000); + right = Math.min(right, 1000); + top = Math.min(top, 1000); + bottom = Math.min(bottom, 1000); + + return new Rect(left, top, right, bottom); + } + + private Area convertMeteringRectangleToArea(Rect sensor_rect, MeteringRectangle metering_rectangle) { + Rect area_rect = convertRectFromCamera2(sensor_rect, metering_rectangle.getRect()); + return new Area(area_rect, metering_rectangle.getMeteringWeight()); + } + + private CameraController.Face convertFromCameraFace(Rect sensor_rect, android.hardware.camera2.params.Face camera2_face) { + Rect area_rect = convertRectFromCamera2(sensor_rect, camera2_face.getBounds()); + return new CameraController.Face(camera2_face.getScore(), area_rect); + } + + @Override + public boolean setFocusAndMeteringArea(List areas) { + Rect sensor_rect = getViewableRect(); + if( MyDebug.LOG ) + Log.d(TAG, "sensor_rect: " + sensor_rect.left + " , " + sensor_rect.top + " x " + sensor_rect.right + " , " + sensor_rect.bottom); + boolean has_focus = false; + boolean has_metering = false; + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) > 0 ) { + has_focus = true; + camera_settings.af_regions = new MeteringRectangle[areas.size()]; + int i = 0; + for(CameraController.Area area : areas) { + camera_settings.af_regions[i++] = convertAreaToMeteringRectangle(sensor_rect, area); + } + camera_settings.setAFRegions(previewBuilder); + } + else + camera_settings.af_regions = null; + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) > 0 ) { + has_metering = true; + camera_settings.ae_regions = new MeteringRectangle[areas.size()]; + int i = 0; + for(CameraController.Area area : areas) { + camera_settings.ae_regions[i++] = convertAreaToMeteringRectangle(sensor_rect, area); + } + camera_settings.setAERegions(previewBuilder); + } + else + camera_settings.ae_regions = null; + if( has_focus || has_metering ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set focus and/or metering regions"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + return has_focus; + } + + @Override + public void clearFocusAndMetering() { + Rect sensor_rect = getViewableRect(); + boolean has_focus = false; + boolean has_metering = false; + if( sensor_rect.width() <= 0 || sensor_rect.height() <= 0 ) { + // had a crash on Google Play due to creating a MeteringRectangle with -ve width/height ?! + camera_settings.af_regions = null; + camera_settings.ae_regions = null; + } + else { + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) > 0 ) { + has_focus = true; + camera_settings.af_regions = new MeteringRectangle[1]; + camera_settings.af_regions[0] = new MeteringRectangle(0, 0, sensor_rect.width()-1, sensor_rect.height()-1, 0); + camera_settings.setAFRegions(previewBuilder); + } + else + camera_settings.af_regions = null; + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) > 0 ) { + has_metering = true; + camera_settings.ae_regions = new MeteringRectangle[1]; + camera_settings.ae_regions[0] = new MeteringRectangle(0, 0, sensor_rect.width()-1, sensor_rect.height()-1, 0); + camera_settings.setAERegions(previewBuilder); + } + else + camera_settings.ae_regions = null; + } + if( has_focus || has_metering ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to clear focus and metering regions"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + } + + @Override + public List getFocusAreas() { + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) == 0 ) + return null; + MeteringRectangle [] metering_rectangles = previewBuilder.get(CaptureRequest.CONTROL_AF_REGIONS); + if( metering_rectangles == null ) + return null; + Rect sensor_rect = getViewableRect(); + camera_settings.af_regions[0] = new MeteringRectangle(0, 0, sensor_rect.width()-1, sensor_rect.height()-1, 0); + if( metering_rectangles.length == 1 && metering_rectangles[0].getRect().left == 0 && metering_rectangles[0].getRect().top == 0 && metering_rectangles[0].getRect().right == sensor_rect.width()-1 && metering_rectangles[0].getRect().bottom == sensor_rect.height()-1 ) { + // for compatibility with CameraController1 + return null; + } + List areas = new ArrayList<>(); + for(MeteringRectangle metering_rectangle : metering_rectangles) { + areas.add(convertMeteringRectangleToArea(sensor_rect, metering_rectangle)); + } + return areas; + } + + @Override + public List getMeteringAreas() { + if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) == 0 ) + return null; + MeteringRectangle [] metering_rectangles = previewBuilder.get(CaptureRequest.CONTROL_AE_REGIONS); + if( metering_rectangles == null ) + return null; + Rect sensor_rect = getViewableRect(); + if( metering_rectangles.length == 1 && metering_rectangles[0].getRect().left == 0 && metering_rectangles[0].getRect().top == 0 && metering_rectangles[0].getRect().right == sensor_rect.width()-1 && metering_rectangles[0].getRect().bottom == sensor_rect.height()-1 ) { + // for compatibility with CameraController1 + return null; + } + List areas = new ArrayList<>(); + for(MeteringRectangle metering_rectangle : metering_rectangles) { + areas.add(convertMeteringRectangleToArea(sensor_rect, metering_rectangle)); + } + return areas; + } + + @Override + public boolean supportsAutoFocus() { + if( previewBuilder.get(CaptureRequest.CONTROL_AF_MODE) == null ) + return false; + int focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( focus_mode == CaptureRequest.CONTROL_AF_MODE_AUTO || focus_mode == CaptureRequest.CONTROL_AF_MODE_MACRO ) + return true; + return false; + } + + @Override + public boolean focusIsContinuous() { + if( previewBuilder == null || previewBuilder.get(CaptureRequest.CONTROL_AF_MODE) == null ) + return false; + int focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE || focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO ) + return true; + return false; + } + + @Override + public boolean focusIsVideo() { + if( previewBuilder.get(CaptureRequest.CONTROL_AF_MODE) == null ) + return false; + int focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO ) { + return true; + } + return false; + } + + @Override + public void setPreviewDisplay(SurfaceHolder holder) throws CameraControllerException { + if( MyDebug.LOG ) { + Log.d(TAG, "setPreviewDisplay"); + Log.e(TAG, "SurfaceHolder not supported for CameraController2!"); + Log.e(TAG, "Should use setPreviewTexture() instead"); + } + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + + @Override + public void setPreviewTexture(SurfaceTexture texture) throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewTexture"); + if( this.texture != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview texture already set"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + this.texture = texture; + } + + private void setRepeatingRequest() throws CameraAccessException { + setRepeatingRequest(previewBuilder.build()); + } + + private void setRepeatingRequest(CaptureRequest request) throws CameraAccessException { + if( MyDebug.LOG ) + Log.d(TAG, "setRepeatingRequest"); + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + return; + } + captureSession.setRepeatingRequest(request, previewCaptureCallback, handler); + if( MyDebug.LOG ) + Log.d(TAG, "setRepeatingRequest done"); + } + + private void capture() throws CameraAccessException { + capture(previewBuilder.build()); + } + + private void capture(CaptureRequest request) throws CameraAccessException { + if( MyDebug.LOG ) + Log.d(TAG, "capture"); + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + return; + } + captureSession.capture(request, previewCaptureCallback, handler); + } + + private void createPreviewRequest() { + if( MyDebug.LOG ) + Log.d(TAG, "createPreviewRequest"); + if( camera == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not available!"); + return; + } + if( MyDebug.LOG ) + Log.d(TAG, "camera: " + camera); + try { + previewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + previewBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW); + camera_settings.setupBuilder(previewBuilder, false); + if( MyDebug.LOG ) + Log.d(TAG, "successfully created preview request"); + } + catch(CameraAccessException e) { + //captureSession = null; + if( MyDebug.LOG ) { + Log.e(TAG, "failed to create capture request"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + + private Surface getPreviewSurface() { + return surface_texture; + } + + private void createCaptureSession(final MediaRecorder video_recorder) throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "create capture session"); + + if( previewBuilder == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "previewBuilder not present!"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + if( camera == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "no camera"); + return; + } + + if( captureSession != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "close old capture session"); + captureSession.close(); + captureSession = null; + } + + try { + captureSession = null; + + if( video_recorder != null ) { + closePictureImageReader(); + } + else { + // in some cases need to recreate picture imageReader and the texture default buffer size (e.g., see test testTakePhotoPreviewPaused()) + createPictureImageReader(); + } + if( texture != null ) { + // need to set the texture size + if( MyDebug.LOG ) + Log.d(TAG, "set size of preview texture"); + if( preview_width == 0 || preview_height == 0 ) { + if( MyDebug.LOG ) + Log.e(TAG, "application needs to call setPreviewSize()"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + texture.setDefaultBufferSize(preview_width, preview_height); + // also need to create a new surface for the texture, in case the size has changed - but make sure we remove the old one first! + if( surface_texture != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove old target: " + surface_texture); + previewBuilder.removeTarget(surface_texture); + } + this.surface_texture = new Surface(texture); + if( MyDebug.LOG ) + Log.d(TAG, "created new target: " + surface_texture); + } + if( video_recorder != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "creating capture session for video recording"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "picture size: " + imageReader.getWidth() + " x " + imageReader.getHeight()); + } + /*if( MyDebug.LOG ) + Log.d(TAG, "preview size: " + previewImageReader.getWidth() + " x " + previewImageReader.getHeight());*/ + if( MyDebug.LOG ) + Log.d(TAG, "preview size: " + this.preview_width + " x " + this.preview_height); + + class MyStateCallback extends CameraCaptureSession.StateCallback { + private boolean callback_done; // must sychronize on this and notifyAll when setting to true + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if( MyDebug.LOG ) { + Log.d(TAG, "onConfigured: " + session); + Log.d(TAG, "captureSession was: " + captureSession); + } + if( camera == null ) { + if( MyDebug.LOG ) { + Log.d(TAG, "camera is closed"); + } + synchronized( create_capture_session_lock ) { + callback_done = true; + create_capture_session_lock.notifyAll(); + } + return; + } + captureSession = session; + Surface surface = getPreviewSurface(); + previewBuilder.addTarget(surface); + if( video_recorder != null ) + previewBuilder.addTarget(video_recorder.getSurface()); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to start preview"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + // we indicate that we failed to start the preview by setting captureSession back to null + // this will cause a CameraControllerException to be thrown below + captureSession = null; + } + synchronized( create_capture_session_lock ) { + callback_done = true; + create_capture_session_lock.notifyAll(); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + if( MyDebug.LOG ) { + Log.d(TAG, "onConfigureFailed: " + session); + Log.d(TAG, "captureSession was: " + captureSession); + } + synchronized( create_capture_session_lock ) { + callback_done = true; + create_capture_session_lock.notifyAll(); + } + // don't throw CameraControllerException here, as won't be caught - instead we throw CameraControllerException below + } + } + final MyStateCallback myStateCallback = new MyStateCallback(); + + Surface preview_surface = getPreviewSurface(); + List surfaces; + if( video_recorder != null ) { + surfaces = Arrays.asList(preview_surface, video_recorder.getSurface()); + } + else if( imageReaderRaw != null ) { + surfaces = Arrays.asList(preview_surface, imageReader.getSurface(), imageReaderRaw.getSurface()); + } + else { + surfaces = Arrays.asList(preview_surface, imageReader.getSurface()); + } + if( MyDebug.LOG ) { + Log.d(TAG, "texture: " + texture); + Log.d(TAG, "preview_surface: " + preview_surface); + if( video_recorder == null ) { + if( imageReaderRaw != null ) { + Log.d(TAG, "imageReaderRaw: " + imageReaderRaw); + Log.d(TAG, "imageReaderRaw: " + imageReaderRaw.getWidth()); + Log.d(TAG, "imageReaderRaw: " + imageReaderRaw.getHeight()); + Log.d(TAG, "imageReaderRaw: " + imageReaderRaw.getImageFormat()); + } + else { + Log.d(TAG, "imageReader: " + imageReader); + Log.d(TAG, "imageReader: " + imageReader.getWidth()); + Log.d(TAG, "imageReader: " + imageReader.getHeight()); + Log.d(TAG, "imageReader: " + imageReader.getImageFormat()); + } + } + } + camera.createCaptureSession(surfaces, + myStateCallback, + handler); + if( MyDebug.LOG ) + Log.d(TAG, "wait until session created..."); + synchronized( create_capture_session_lock ) { + while( !myStateCallback.callback_done ) { + try { + // release the lock, and wait until myStateCallback calls notifyAll() + create_capture_session_lock.wait(); + } + catch(InterruptedException e) { + e.printStackTrace(); + } + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "created captureSession: " + captureSession); + } + if( captureSession == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to create capture session"); + throw new CameraControllerException(); + } + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "CameraAccessException trying to create capture session"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void startPreview() throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "startPreview"); + if( captureSession != null ) { + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to start preview"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + // do via CameraControllerException instead of preview_error_cb, so caller immediately knows preview has failed + throw new CameraControllerException(); + } + return; + } + createCaptureSession(null); + } + + @Override + public void stopPreview() { + if( MyDebug.LOG ) + Log.d(TAG, "stopPreview"); + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + return; + } + try { + captureSession.stopRepeating(); + // although stopRepeating() alone will pause the preview, seems better to close captureSession altogether - this allows the app to make changes such as changing the picture size + if( MyDebug.LOG ) + Log.d(TAG, "close capture session"); + captureSession.close(); + captureSession = null; + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to stop repeating"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + // simulate CameraController1 behaviour where face detection is stopped when we stop preview + if( camera_settings.has_face_detect_mode ) { + if( MyDebug.LOG ) + Log.d(TAG, "cancel face detection"); + camera_settings.has_face_detect_mode = false; + camera_settings.setFaceDetectMode(previewBuilder); + // no need to call setRepeatingRequest(), we're just setting the camera_settings for when we restart the preview + } + } + + @Override + public boolean startFaceDetection() { + if( previewBuilder.get(CaptureRequest.STATISTICS_FACE_DETECT_MODE) != null && previewBuilder.get(CaptureRequest.STATISTICS_FACE_DETECT_MODE) == CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) { + return false; + } + camera_settings.has_face_detect_mode = true; + camera_settings.face_detect_mode = CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL; + camera_settings.setFaceDetectMode(previewBuilder); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to start face detection"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + return true; + } + + @Override + public void setFaceDetectionListener(final FaceDetectionListener listener) { + this.face_detection_listener = listener; + } + + @Override + public void autoFocus(final AutoFocusCallback cb, boolean capture_follows_autofocus_hint) { + if( MyDebug.LOG ) { + Log.d(TAG, "autoFocus"); + Log.d(TAG, "capture_follows_autofocus_hint? " + capture_follows_autofocus_hint); + } + fake_precapture_torch_focus_performed = false; + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + // should call the callback, so the application isn't left waiting (e.g., when we autofocus before trying to take a photo) + cb.onAutoFocus(false); + return; + } + Integer focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( focus_mode == null ) { + // we preserve the old Camera API where calling autoFocus() on a device without autofocus immediately calls the callback + // (unclear if Open Camera needs this, but just to be safe and consistent between camera APIs) + cb.onAutoFocus(true); + return; + } + else if( focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) { + /* In the old Camera API, doing an autofocus in FOCUS_MODE_CONTINUOUS_PICTURE mode would call the callback when the camera isn't focusing, + * and return whether focus was successful or not. So we replicate the behaviour here too (see previewCaptureCallback.process()). + * This is essential to have correct behaviour for flash mode in continuous picture focus mode. Otherwise: + * - Taking photo with flash auto when flash is used, or flash on, takes longer (excessive amount of flash firing due to an additional unnecessary focus before taking photo). + * - Taking photo with flash auto when flash is needed sometime results in flash firing for the (unnecessary) autofocus, then not firing for final picture, resulting in too dark pictures. + * This seems to happen with scenes that have both light and dark regions. + * (All tested on Nexus 6, Android 6.) + */ + this.capture_follows_autofocus_hint = capture_follows_autofocus_hint; + this.autofocus_cb = cb; + return; + } + /*if( state == STATE_WAITING_AUTOFOCUS ) { + if( MyDebug.LOG ) + Log.d(TAG, "already waiting for an autofocus"); + // need to update the callback! + this.capture_follows_autofocus_hint = capture_follows_autofocus_hint; + this.autofocus_cb = cb; + return; + }*/ + CaptureRequest.Builder afBuilder = previewBuilder; + if( MyDebug.LOG ) { + { + MeteringRectangle [] areas = afBuilder.get(CaptureRequest.CONTROL_AF_REGIONS); + for(int i=0;areas != null && i 1.0 ) + alpha = 1.0; + if( MyDebug.LOG ) { + Log.d(TAG, "exposure_time: " + exposure_time); + Log.d(TAG, "alpha: " + alpha); + } + // alpha==0 means exposure_time_scale==1; alpha==1 means exposure_time_scale==full_exposure_time_scale + return (1.0 - alpha) + alpha * full_exposure_time_scale; + } + + private void takePictureAfterPrecapture() { + if( MyDebug.LOG ) + Log.d(TAG, "takePictureAfterPrecapture"); + if( want_expo_bracketing ) { + takePictureBurstExpoBracketing(); + return; + } + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + return; + } + try { + if( MyDebug.LOG ) { + if( imageReaderRaw != null ) { + Log.d(TAG, "imageReaderRaw: " + imageReaderRaw.toString()); + Log.d(TAG, "imageReaderRaw surface: " + imageReaderRaw.getSurface().toString()); + } + else { + Log.d(TAG, "imageReader: " + imageReader.toString()); + Log.d(TAG, "imageReader surface: " + imageReader.getSurface().toString()); + } + } + CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + stillBuilder.setTag(RequestTag.CAPTURE); + camera_settings.setupBuilder(stillBuilder, true); + if( use_fake_precapture_mode && fake_precapture_torch_performed ) { + if( MyDebug.LOG ) + Log.d(TAG, "setting torch for capture"); + stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + stillBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); + } + if( !camera_settings.has_iso && this.optimise_ae_for_dro && capture_result_has_exposure_time && (camera_settings.flash_value.equals("flash_off") || camera_settings.flash_value.equals("flash_auto") || camera_settings.flash_value.equals("flash_frontscreen_auto") ) ) { + final double full_exposure_time_scale = Math.pow(2.0, -0.5); + final long fixed_exposure_time = 1000000000L/60; // we only scale the exposure time at all if it's less than this value + final long scaled_exposure_time = 1000000000L/120; // we only scale the exposure time by the full_exposure_time_scale if the exposure time is less than this value + long exposure_time = capture_result_exposure_time; + if( exposure_time <= fixed_exposure_time ) { + Range exposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); + if( exposure_time_range != null ) { + double exposure_time_scale = getScaleForExposureTime(exposure_time, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale); + if (MyDebug.LOG) { + Log.d(TAG, "reduce exposure shutter speed further, was: " + exposure_time); + Log.d(TAG, "exposure_time_scale: " + exposure_time_scale); + } + long min_exposure_time = exposure_time_range.getLower(); + long max_exposure_time = exposure_time_range.getUpper(); + exposure_time *= exposure_time_scale; + if( exposure_time < min_exposure_time ) + exposure_time = min_exposure_time; + if( exposure_time > max_exposure_time ) + exposure_time = max_exposure_time; + if (MyDebug.LOG) { + Log.d(TAG, "exposure_time: " + exposure_time); + } + stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF); + if( capture_result_has_iso ) + stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, capture_result_iso ); + else + stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, 800); + if( capture_result_has_frame_duration ) + stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, capture_result_frame_duration); + else + stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, 1000000000L/30); + stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposure_time); + } + } + } + //stillBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + //stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + clearPending(); + Surface surface = getPreviewSurface(); + stillBuilder.addTarget(surface); // Google Camera adds the preview surface as well as capture surface, for still capture + stillBuilder.addTarget(imageReader.getSurface()); + if( imageReaderRaw != null ) + stillBuilder.addTarget(imageReaderRaw.getSurface()); + + captureSession.stopRepeating(); // need to stop preview before capture (as done in Camera2Basic; otherwise we get bugs such as flash remaining on after taking a photo with flash) + if( jpeg_cb != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "call onStarted() in callback"); + jpeg_cb.onStarted(); + } + if( MyDebug.LOG ) + Log.d(TAG, "capture with stillBuilder"); + captureSession.capture(stillBuilder.build(), previewCaptureCallback, handler); + if( sounds_enabled ) // play shutter sound asap, otherwise user has the illusion of being slow to take photos + media_action_sound.play(MediaActionSound.SHUTTER_CLICK); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to take picture"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + jpeg_cb = null; + if( take_picture_error_cb != null ) { + take_picture_error_cb.onError(); + take_picture_error_cb = null; + } + } + } + + private void takePictureBurstExpoBracketing() { + if( MyDebug.LOG ) + Log.d(TAG, "takePictureBurstExpBracketing"); + if( MyDebug.LOG && !want_expo_bracketing ) { + Log.e(TAG, "takePictureBurstExpoBracketing called but want_expo_bracketing is false"); + } + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + return; + } + try { + if( MyDebug.LOG ) { + Log.d(TAG, "imageReader: " + imageReader.toString()); + Log.d(TAG, "imageReader surface: " + imageReader.getSurface().toString()); + } + + CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + // n.b., don't set RequestTag.CAPTURE here - we only do it for the last of the burst captures (see below) + camera_settings.setupBuilder(stillBuilder, true); + clearPending(); + Surface surface = getPreviewSurface(); + stillBuilder.addTarget(surface); // Google Camera adds the preview surface as well as capture surface, for still capture + stillBuilder.addTarget(imageReader.getSurface()); + + List requests = new ArrayList<>(); + + /*stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + stillBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF); + + stillBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, -6); + requests.add( stillBuilder.build() ); + stillBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 0); + requests.add( stillBuilder.build() ); + stillBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 6); + requests.add( stillBuilder.build() );*/ + + stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF); + if( use_fake_precapture_mode && fake_precapture_torch_performed ) { + stillBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); + } + // else don't turn torch off, as user may be in torch on mode + + // obtain current ISO/etc settings from the capture result - but if we're in manual ISO mode, + // might as well use the settings the user has actually requested (also useful for workaround for + // OnePlus 3T bug where the reported ISO and exposure_time are wrong in dark scenes) + if( camera_settings.has_iso ) + stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, camera_settings.iso ); + else if( capture_result_has_iso ) + stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, capture_result_iso ); + else + stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, 800); + if( capture_result_has_frame_duration ) + stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, capture_result_frame_duration); + else + stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, 1000000000L/30); + + long base_exposure_time = 1000000000L/30; + if( camera_settings.has_iso ) + base_exposure_time = camera_settings.exposure_time; + else if( capture_result_has_exposure_time ) + base_exposure_time = capture_result_exposure_time; + + int n_half_images = expo_bracketing_n_images/2; + long min_exposure_time = base_exposure_time; + long max_exposure_time = base_exposure_time; + final double scale = Math.pow(2.0, expo_bracketing_stops); + Range exposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); + if( exposure_time_range != null ) { + min_exposure_time = exposure_time_range.getLower(); + max_exposure_time = exposure_time_range.getUpper(); + } + + if( MyDebug.LOG ) { + Log.d(TAG, "taking expo bracketing with n_images: " + expo_bracketing_n_images); + Log.d(TAG, "ISO: " + stillBuilder.get(CaptureRequest.SENSOR_SENSITIVITY)); + Log.d(TAG, "Frame duration: " + stillBuilder.get(CaptureRequest.SENSOR_FRAME_DURATION)); + Log.d(TAG, "Base exposure time: " + base_exposure_time); + Log.d(TAG, "Min exposure time: " + min_exposure_time); + Log.d(TAG, "Max exposure time: " + max_exposure_time); + } + + // darker images + for(int i=0;i max_exposure_time ) + exposure_time = max_exposure_time; + if( MyDebug.LOG ) { + Log.d(TAG, "add burst request for " + i + "th light image:"); + Log.d(TAG, " this_scale: " + this_scale); + Log.d(TAG, " exposure_time: " + exposure_time); + } + stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposure_time); + if( i == n_half_images - 1 ) { + // RequestTag.CAPTURE should only be set for the last request, otherwise we'll may do things like turning + // off torch (for fake flash) before all images are received + // More generally, doesn't seem a good idea to be doing the post-capture commands (resetting ae state etc) + // multiple times, and before all captures are complete! + if( MyDebug.LOG ) + Log.d(TAG, "set RequestTag.CAPTURE for last burst request"); + stillBuilder.setTag(RequestTag.CAPTURE); + } + requests.add( stillBuilder.build() ); + } + } + + n_burst = requests.size(); + if( MyDebug.LOG ) + Log.d(TAG, "n_burst: " + n_burst); + + captureSession.stopRepeating(); // see note under takePictureAfterPrecapture() + + if( jpeg_cb != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "call onStarted() in callback"); + jpeg_cb.onStarted(); + } + + if( use_expo_fast_burst ) { + if( MyDebug.LOG ) + Log.d(TAG, "using fast burst"); + int sequenceId = captureSession.captureBurst(requests, previewCaptureCallback, handler); + if( MyDebug.LOG ) + Log.d(TAG, "sequenceId: " + sequenceId); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "using slow burst"); + burst_capture_requests = requests; + burst_start_ms = System.currentTimeMillis(); + captureSession.capture(requests.get(0), previewCaptureCallback, handler); + } + + if( sounds_enabled ) // play shutter sound asap, otherwise user has the illusion of being slow to take photos + media_action_sound.play(MediaActionSound.SHUTTER_CLICK); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to take picture burst"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + jpeg_cb = null; + if( take_picture_error_cb != null ) { + take_picture_error_cb.onError(); + take_picture_error_cb = null; + } + } + } + + private void runPrecapture() { + if( MyDebug.LOG ) + Log.d(TAG, "runPrecapture"); + // first run precapture sequence + if( MyDebug.LOG ) { + if( use_fake_precapture_mode ) + Log.e(TAG, "shouldn't be doing standard precapture when use_fake_precapture_mode is true!"); + else if( want_expo_bracketing ) + Log.e(TAG, "shouldn't be doing precapture for want_expo_bracketing - should be using fake precapture!"); + } + try { + // use a separate builder for precapture - otherwise have problem that if we take photo with flash auto/on of dark scene, then point to a bright scene, the autoexposure isn't running until we autofocus again + final CaptureRequest.Builder precaptureBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + precaptureBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + + camera_settings.setupBuilder(precaptureBuilder, false); + precaptureBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + precaptureBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + precaptureBuilder.addTarget(getPreviewSurface()); + + state = STATE_WAITING_PRECAPTURE_START; + precapture_state_change_time_ms = System.currentTimeMillis(); + + // first set precapture to idle - this is needed, otherwise we hang in state STATE_WAITING_PRECAPTURE_START, because precapture already occurred whilst autofocusing, and it doesn't occur again unless we first set the precapture trigger to idle + if( MyDebug.LOG ) + Log.d(TAG, "capture with precaptureBuilder"); + captureSession.capture(precaptureBuilder.build(), previewCaptureCallback, handler); + captureSession.setRepeatingRequest(precaptureBuilder.build(), previewCaptureCallback, handler); + + // now set precapture + precaptureBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START); + captureSession.capture(precaptureBuilder.build(), previewCaptureCallback, handler); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to precapture"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + jpeg_cb = null; + if( take_picture_error_cb != null ) { + take_picture_error_cb.onError(); + take_picture_error_cb = null; + } + } + } + + private void runFakePrecapture() { + if( MyDebug.LOG ) + Log.d(TAG, "runFakePrecapture"); + if( camera_settings.flash_value.equals("flash_auto") || camera_settings.flash_value.equals("flash_on") ) { + if( MyDebug.LOG ) + Log.d(TAG, "turn on torch"); + previewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + previewBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); + fake_precapture_torch_performed = true; + } + else if( camera_settings.flash_value.equals("flash_frontscreen_auto") || camera_settings.flash_value.equals("flash_frontscreen_on") ) { + if( jpeg_cb != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "request screen turn on for frontscreen flash"); + jpeg_cb.onFrontScreenTurnOn(); + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "can't request screen turn on for frontscreen flash, as no jpeg_cb"); + } + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "runFakePrecapture called with unexpected flash value: " + camera_settings.flash_value); + } + state = STATE_WAITING_FAKE_PRECAPTURE_START; + precapture_state_change_time_ms = System.currentTimeMillis(); + fake_precapture_turn_on_torch_id = null; + try { + CaptureRequest request = previewBuilder.build(); + if( fake_precapture_torch_performed ) { + fake_precapture_turn_on_torch_id = request; + if( MyDebug.LOG ) + Log.d(TAG, "fake_precapture_turn_on_torch_id: " + request); + } + setRepeatingRequest(request); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to start fake precapture"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + jpeg_cb = null; + if( take_picture_error_cb != null ) { + take_picture_error_cb.onError(); + take_picture_error_cb = null; + } + } + } + + /** Used in use_fake_precapture mode when flash is auto, this returns whether we fire the flash. + * If the decision was recently calculated, we return that same decision - used to fix problem that if + * we fire flash during autofocus (for autofocus mode), we don't then want to decide the scene is too + * bright to not need flash for taking photo! + */ + private boolean fireAutoFlash() { + if( MyDebug.LOG ) + Log.d(TAG, "fireAutoFlash"); + long time_now = System.currentTimeMillis(); + if( MyDebug.LOG && fake_precapture_use_flash_time_ms != -1 ) { + Log.d(TAG, "fake_precapture_use_flash_time_ms: " + fake_precapture_use_flash_time_ms); + Log.d(TAG, "time_now: " + time_now); + Log.d(TAG, "time since last flash auto decision: " + (time_now - fake_precapture_use_flash_time_ms)); + } + final long cache_time_ms = 3000; // needs to be at least the time of a typical autoflash, see comment for this function above + if( fake_precapture_use_flash_time_ms != -1 && time_now - fake_precapture_use_flash_time_ms < cache_time_ms ) { + if( MyDebug.LOG ) + Log.d(TAG, "use recent decision: " + fake_precapture_use_flash); + fake_precapture_use_flash_time_ms = time_now; + return fake_precapture_use_flash; + } + fake_precapture_use_flash_time_ms = time_now; + if( camera_settings.flash_value.equals("flash_auto") ) { + fake_precapture_use_flash = capture_result_needs_flash; + } + else if( camera_settings.flash_value.equals("flash_frontscreen_auto") ) { + // iso_threshold fine-tuned for Nexus 6 - front camera ISO never goes above 805, but a threshold of 700 is too low + int iso_threshold = camera_settings.flash_value.equals("flash_frontscreen_auto") ? 750 : 1000; + fake_precapture_use_flash = capture_result_has_iso && capture_result_iso >= iso_threshold; + if( MyDebug.LOG ) + Log.d(TAG, " ISO was: " + capture_result_iso); + } + else { + // shouldn't really be calling this function if not flash auto... + fake_precapture_use_flash = false; + } + if( MyDebug.LOG ) + Log.d(TAG, "fake_precapture_use_flash: " + fake_precapture_use_flash); + return fake_precapture_use_flash; + } + + @Override + public void takePicture(final PictureCallback picture, final ErrorCallback error) { + if( MyDebug.LOG ) + Log.d(TAG, "takePicture"); + if( camera == null || captureSession == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "no camera or capture session"); + error.onError(); + return; + } + // we store as two identical callbacks, so we can independently set each to null as the two callbacks occur + this.jpeg_cb = picture; + if( imageReaderRaw != null ) + this.raw_cb = picture; + else + this.raw_cb = null; + this.take_picture_error_cb = error; + this.fake_precapture_torch_performed = false; // just in case still on? + if( !ready_for_capture ) { + if( MyDebug.LOG ) + Log.e(TAG, "takePicture: not ready for capture!"); + //throw new RuntimeException(); // debugging + } + + { + if( MyDebug.LOG ) { + Log.d(TAG, "current flash value: " + camera_settings.flash_value); + Log.d(TAG, "use_fake_precapture_mode: " + use_fake_precapture_mode); + } + // Don't need precapture if flash off or torch + // And currently has_iso manual mode doesn't support flash - but just in case that's changed later, we still probably don't want to be doing a precapture... + if( camera_settings.has_iso || camera_settings.flash_value.equals("flash_off") || camera_settings.flash_value.equals("flash_torch") ) { + takePictureAfterPrecapture(); + } + else if( use_fake_precapture_mode ) { + // fake precapture works by turning on torch (or using a "front screen flash"), so we can't use the camera's own decision for flash auto + // instead we check the current ISO value + boolean auto_flash = camera_settings.flash_value.equals("flash_auto") || camera_settings.flash_value.equals("flash_frontscreen_auto"); + Integer flash_mode = previewBuilder.get(CaptureRequest.FLASH_MODE); + if( MyDebug.LOG ) + Log.d(TAG, "flash_mode: " + flash_mode); + if( auto_flash && !fireAutoFlash() ) { + if( MyDebug.LOG ) + Log.d(TAG, "fake precapture flash auto: seems bright enough to not need flash"); + takePictureAfterPrecapture(); + } + else if( flash_mode != null && flash_mode == CameraMetadata.FLASH_MODE_TORCH ) { + if( MyDebug.LOG ) + Log.d(TAG, "fake precapture flash: torch already on (presumably from autofocus)"); + // On some devices (e.g., OnePlus 3T), if we've already turned on torch for an autofocus immediately before + // taking the photo, ae convergence may have already occurred - so if we called runFakePrecapture(), we'd just get + // stuck waiting for CONTROL_AE_STATE_SEARCHING which will never happen, until we hit the timeout - it works, + // but it means taking photos is slower as we have to wait until the timeout + // Instead we assume that ae scanning has already started, so go straight to STATE_WAITING_FAKE_PRECAPTURE_DONE, + // which means wait until we're no longer CONTROL_AE_STATE_SEARCHING. + // (Note, we don't want to go straight to takePictureAfterPrecapture(), as it might be that ae scanning is still + // taking place.) + // An alternative solution would be to switch torch off and back on again to cause ae scanning to start - but + // at worst this is tricky to get working, and at best, taking photos would be slower. + fake_precapture_torch_performed = true; // so we know to fire the torch when capturing + state = STATE_WAITING_FAKE_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + } + else { + runFakePrecapture(); + } + } + else { + // standard flash, flash auto or on + if( camera_settings.flash_value.equals("flash_auto") && !capture_result_needs_flash ) { + // if we call precapture anyway, flash wouldn't fire - but we tend to have a pause + // so skipping the precapture if flash isn't going to fire makes this faster + if( MyDebug.LOG ) + Log.d(TAG, "flash auto, but we don't need flash"); + takePictureAfterPrecapture(); + } + else { + runPrecapture(); + } + } + } + + /*camera_settings.setupBuilder(previewBuilder, false); + previewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + state = STATE_WAITING_AUTOFOCUS; + precapture_started = -1; + //capture(); + setRepeatingRequest();*/ + } + + @Override + public void setDisplayOrientation(int degrees) { + // for CameraController2, the preview display orientation is handled via the TextureView's transform + if( MyDebug.LOG ) + Log.d(TAG, "setDisplayOrientation not supported by this API"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + + @Override + public int getDisplayOrientation() { + if( MyDebug.LOG ) + Log.d(TAG, "getDisplayOrientation not supported by this API"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + + @Override + public int getCameraOrientation() { + return characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + } + + @Override + public boolean isFrontFacing() { + return characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT; + } + + @Override + public void unlock() { + // do nothing at this stage + } + + @Override + public void initVideoRecorderPrePrepare(MediaRecorder video_recorder) { + // if we change where we play the START_VIDEO_RECORDING sound, make sure it can't be heard in resultant video + if( sounds_enabled ) + media_action_sound.play(MediaActionSound.START_VIDEO_RECORDING); + } + + @Override + public void initVideoRecorderPostPrepare(MediaRecorder video_recorder) throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "initVideoRecorderPostPrepare"); + try { + if( MyDebug.LOG ) + Log.d(TAG, "obtain video_recorder surface"); + if( MyDebug.LOG ) + Log.d(TAG, "done"); + previewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + previewBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_RECORD); + camera_settings.setupBuilder(previewBuilder, false); + createCaptureSession(video_recorder); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to create capture request for video"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + throw new CameraControllerException(); + } + } + + @Override + public void reconnect() throws CameraControllerException { + if( MyDebug.LOG ) + Log.d(TAG, "reconnect"); + // if we change where we play the STOP_VIDEO_RECORDING sound, make sure it can't be heard in resultant video + if( sounds_enabled ) + media_action_sound.play(MediaActionSound.STOP_VIDEO_RECORDING); + createPreviewRequest(); + createCaptureSession(null); + /*if( MyDebug.LOG ) + Log.d(TAG, "add preview surface to previewBuilder"); + Surface surface = getPreviewSurface(); + previewBuilder.addTarget(surface);*/ + //setRepeatingRequest(); + } + + @Override + public String getParametersString() { + return null; + } + + @Override + public boolean captureResultIsAEScanning() { + return capture_result_is_ae_scanning; + } + + @Override + public boolean captureResultHasIso() { + return capture_result_has_iso; + } + + @Override + public int captureResultIso() { + return capture_result_iso; + } + + @Override + public boolean captureResultHasExposureTime() { + return capture_result_has_exposure_time; + } + + @Override + public long captureResultExposureTime() { + return capture_result_exposure_time; + } + + /* + @Override + public boolean captureResultHasFrameDuration() { + return capture_result_has_frame_duration; + } + + @Override + public long captureResultFrameDuration() { + return capture_result_frame_duration; + } + + @Override + public boolean captureResultHasFocusDistance() { + return capture_result_has_focus_distance; + } + + @Override + public float captureResultFocusDistanceMin() { + return capture_result_focus_distance_min; + } + + @Override + public float captureResultFocusDistanceMax() { + return capture_result_focus_distance_max; + } + */ + + private final CameraCaptureSession.CaptureCallback previewCaptureCallback = new CameraCaptureSession.CaptureCallback() { + private long last_process_frame_number = 0; + private int last_af_state = -1; + + public void onCaptureBufferLost(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, Surface target, long frameNumber) { + if( MyDebug.LOG ) + Log.d(TAG, "onCaptureBufferLost: " + frameNumber); + super.onCaptureBufferLost(session, request, target, frameNumber); + } + + public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, CaptureFailure failure) { + if( MyDebug.LOG ) + Log.d(TAG, "onCaptureFailed: " + failure); + super.onCaptureFailed(session, request, failure); // API docs say this does nothing, but call it just to be safe + } + + public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) { + if( MyDebug.LOG ) { + Log.d(TAG, "onCaptureSequenceAborted"); + Log.d(TAG, "sequenceId: " + sequenceId); + } + super.onCaptureSequenceAborted(session, sequenceId); // API docs say this does nothing, but call it just to be safe + } + + public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) { + if( MyDebug.LOG ) { + Log.d(TAG, "onCaptureSequenceCompleted"); + Log.d(TAG, "sequenceId: " + sequenceId); + Log.d(TAG, "frameNumber: " + frameNumber); + } + super.onCaptureSequenceCompleted(session, sequenceId, frameNumber); // API docs say this does nothing, but call it just to be safe + } + + public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { + if( request.getTag() == RequestTag.CAPTURE ) { + if( MyDebug.LOG ) + Log.d(TAG, "onCaptureStarted: capture"); + // n.b., we don't play the shutter sound here, as it typically sounds "too late" + // (if ever we changed this, would also need to fix for burst, where we only set the RequestTag.CAPTURE for the last image) + } + super.onCaptureStarted(session, request, timestamp, frameNumber); + } + + public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { + /*if( MyDebug.LOG ) + Log.d(TAG, "onCaptureProgressed");*/ + process(request, partialResult); + super.onCaptureProgressed(session, request, partialResult); // API docs say this does nothing, but call it just to be safe (as with Google Camera) + } + + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + if( request.getTag() == RequestTag.CAPTURE ) { + if (MyDebug.LOG) { + Log.d(TAG, "onCaptureCompleted: capture"); + Log.d(TAG, "sequenceId: " + result.getSequenceId()); + } + } + process(request, result); + processCompleted(request, result); + super.onCaptureCompleted(session, request, result); // API docs say this does nothing, but call it just to be safe (as with Google Camera) + } + + /** Processes either a partial or total result. + */ + private void process(CaptureRequest request, CaptureResult result) { + /*if( MyDebug.LOG ) + Log.d(TAG, "process, state: " + state);*/ + if( result.getFrameNumber() < last_process_frame_number ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "processAF discarded outdated frame " + result.getFrameNumber() + " vs " + last_process_frame_number);*/ + return; + } + last_process_frame_number = result.getFrameNumber(); + + /*Integer flash_mode = result.get(CaptureResult.FLASH_MODE); + if( MyDebug.LOG ) { + if( flash_mode == null ) + Log.d(TAG, "FLASH_MODE is null"); + else if( flash_mode == CaptureResult.FLASH_MODE_OFF ) + Log.d(TAG, "FLASH_MODE = FLASH_MODE_OFF"); + else if( flash_mode == CaptureResult.FLASH_MODE_SINGLE ) + Log.d(TAG, "FLASH_MODE = FLASH_MODE_SINGLE"); + else if( flash_mode == CaptureResult.FLASH_MODE_TORCH ) + Log.d(TAG, "FLASH_MODE = FLASH_MODE_TORCH"); + else + Log.d(TAG, "FLASH_MODE = " + flash_mode); + }*/ + + // use Integer instead of int, so can compare to null: Google Play crashes confirmed that this can happen; Google Camera also ignores cases with null af state + Integer af_state = result.get(CaptureResult.CONTROL_AF_STATE); + /*if( MyDebug.LOG ) { + if( af_state == null ) + Log.d(TAG, "CONTROL_AF_STATE is null"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_INACTIVE ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_INACTIVE"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_PASSIVE_SCAN"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_PASSIVE_FOCUSED"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_ACTIVE_SCAN"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_FOCUSED_LOCKED"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_NOT_FOCUSED_LOCKED"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED ) + Log.d(TAG, "CONTROL_AF_STATE = CONTROL_AF_STATE_PASSIVE_UNFOCUSED"); + else + Log.d(TAG, "CONTROL_AF_STATE = " + af_state); + }*/ + if( af_state != null && af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "not ready for capture: " + af_state);*/ + ready_for_capture = false; + } + else { + /*if( MyDebug.LOG ) + Log.d(TAG, "ready for capture: " + af_state);*/ + ready_for_capture = true; + if( autofocus_cb != null && focusIsContinuous() ) { + Integer focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( focus_mode != null && focus_mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) { + if( MyDebug.LOG ) + Log.d(TAG, "call autofocus callback, as continuous mode and not focusing: " + af_state); + // need to check af_state != null, I received Google Play crash in 1.33 where it was null + boolean focus_success = af_state != null && ( af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED ); + if( MyDebug.LOG ) { + if( focus_success ) + Log.d(TAG, "autofocus success"); + else + Log.d(TAG, "autofocus failed"); + if( af_state == null ) + Log.e(TAG, "continuous focus mode but af_state is null"); + else + Log.d(TAG, "af_state: " + af_state); + } + autofocus_cb.onAutoFocus(focus_success); + autofocus_cb = null; + capture_follows_autofocus_hint = false; + } + } + } + + /*if( MyDebug.LOG ) { + if( autofocus_cb == null ) { + if( af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ) + Log.d(TAG, "processAF: autofocus success but no callback set"); + else if( af_state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED ) + Log.d(TAG, "processAF: autofocus failed but no callback set"); + } + }*/ + // CONTROL_AE_STATE can be null on some devices, so as with af_state, use Integer + Integer ae_state = result.get(CaptureResult.CONTROL_AE_STATE); + /*if( MyDebug.LOG ) { + if( ae_state == null ) + Log.d(TAG, "CONTROL_AE_STATE is null"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_INACTIVE ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_INACTIVE"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_SEARCHING ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_SEARCHING"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_CONVERGED ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_CONVERGED"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_LOCKED ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_LOCKED"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_FLASH_REQUIRED"); + else if( ae_state == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ) + Log.d(TAG, "CONTROL_AE_STATE = CONTROL_AE_STATE_PRECAPTURE"); + else + Log.d(TAG, "CONTROL_AE_STATE = " + ae_state); + }*/ + if( ae_state != null && ae_state == CaptureResult.CONTROL_AE_STATE_SEARCHING ) { + /*if( MyDebug.LOG && !capture_result_is_ae_scanning ) + Log.d(TAG, "ae_state now searching");*/ + capture_result_is_ae_scanning = true; + } + else { + /*if( MyDebug.LOG && capture_result_is_ae_scanning ) + Log.d(TAG, "ae_state stopped searching");*/ + capture_result_is_ae_scanning = false; + } + + if( ae_state != null && ae_state == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ) { + if( MyDebug.LOG && !capture_result_needs_flash ) + Log.d(TAG, "ae_state now needs flash"); + capture_result_needs_flash = true; + } + else { + if( MyDebug.LOG && capture_result_needs_flash ) + Log.d(TAG, "ae_state no longer needs flash"); + capture_result_needs_flash = false; + } + + /*Integer awb_state = result.get(CaptureResult.CONTROL_AWB_STATE); + if( MyDebug.LOG ) { + if( awb_state == null ) + Log.d(TAG, "CONTROL_AWB_STATE is null"); + else if( awb_state == CaptureResult.CONTROL_AWB_STATE_INACTIVE ) + Log.d(TAG, "CONTROL_AWB_STATE = CONTROL_AWB_STATE_INACTIVE"); + else if( awb_state == CaptureResult.CONTROL_AWB_STATE_SEARCHING ) + Log.d(TAG, "CONTROL_AWB_STATE = CONTROL_AWB_STATE_SEARCHING"); + else if( awb_state == CaptureResult.CONTROL_AWB_STATE_CONVERGED ) + Log.d(TAG, "CONTROL_AWB_STATE = CONTROL_AWB_STATE_CONVERGED"); + else if( awb_state == CaptureResult.CONTROL_AWB_STATE_LOCKED ) + Log.d(TAG, "CONTROL_AWB_STATE = CONTROL_AWB_STATE_LOCKED"); + else + Log.d(TAG, "CONTROL_AWB_STATE = " + awb_state); + }*/ + + if( fake_precapture_turn_on_torch_id != null && fake_precapture_turn_on_torch_id == request ) { + if( MyDebug.LOG ) + Log.d(TAG, "torch turned on for fake precapture"); + fake_precapture_turn_on_torch_id = null; + } + + if( state == STATE_NORMAL ) { + // do nothing + } + else if( state == STATE_WAITING_AUTOFOCUS ) { + if( af_state == null ) { + // autofocus shouldn't really be requested if af not available, but still allow this rather than getting stuck waiting for autofocus to complete + if( MyDebug.LOG ) + Log.e(TAG, "waiting for autofocus but af_state is null"); + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + if( autofocus_cb != null ) { + autofocus_cb.onAutoFocus(false); + autofocus_cb = null; + } + capture_follows_autofocus_hint = false; + } + else if( af_state != last_af_state ) { + // check for autofocus completing + // need to check that af_state != last_af_state, except for continuous focus mode where if we're already focused, should return immediately + if( af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED || + af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED || af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED + ) { + boolean focus_success = af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED; + if( MyDebug.LOG ) { + if( focus_success ) + Log.d(TAG, "onCaptureCompleted: autofocus success"); + else + Log.d(TAG, "onCaptureCompleted: autofocus failed"); + Log.d(TAG, "af_state: " + af_state); + } + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + if( use_fake_precapture_mode && fake_precapture_torch_focus_performed ) { + fake_precapture_torch_focus_performed = false; + if( !capture_follows_autofocus_hint ) { + // If we're going to be taking a photo immediately after the autofocus, it's better for the fake flash + // mode to leave the torch on. If we don't do this, one of the following issues can happen: + // - On OnePlus 3T, the torch doesn't get turned off, but because we've switched off the torch flag + // in previewBuilder, we go ahead with the precapture routine instead of + if( MyDebug.LOG ) + Log.d(TAG, "turn off torch after focus (fake precapture code)"); + + // same hack as in setFlashValue() - for fake precapture we need to turn off the torch mode that was set, but + // at least on Nexus 6, we need to turn to flash_off to turn off the torch! + String saved_flash_value = camera_settings.flash_value; + camera_settings.flash_value = "flash_off"; + camera_settings.setAEMode(previewBuilder, false); + try { + capture(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to do capture to turn off torch after autofocus"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + + // now set the actual (should be flash auto or flash on) mode + camera_settings.flash_value = saved_flash_value; + camera_settings.setAEMode(previewBuilder, false); + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set repeating request to turn off torch after autofocus"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "torch was enabled for autofocus, leave it on for capture (fake precapture code)"); + } + } + if( autofocus_cb != null ) { + autofocus_cb.onAutoFocus(focus_success); + autofocus_cb = null; + } + capture_follows_autofocus_hint = false; + } + } + } + else if( state == STATE_WAITING_PRECAPTURE_START ) { + if( MyDebug.LOG ) + Log.d(TAG, "waiting for precapture start..."); + if( MyDebug.LOG ) { + if( ae_state != null ) + Log.d(TAG, "CONTROL_AE_STATE = " + ae_state); + else + Log.d(TAG, "CONTROL_AE_STATE is null"); + } + if( ae_state == null || ae_state == CaptureResult.CONTROL_AE_STATE_PRECAPTURE /*|| ae_state == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED*/ ) { + // we have to wait for CONTROL_AE_STATE_PRECAPTURE; if we allow CONTROL_AE_STATE_FLASH_REQUIRED, then on Nexus 6 at least we get poor quality results with flash: + // varying levels of brightness, sometimes too bright or too dark, sometimes with blue tinge, sometimes even with green corruption + if( MyDebug.LOG ) { + Log.d(TAG, "precapture started after: " + (System.currentTimeMillis() - precapture_state_change_time_ms)); + } + state = STATE_WAITING_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + } + else if( precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > precapture_start_timeout_c ) { + // hack - give up waiting - sometimes we never get a CONTROL_AE_STATE_PRECAPTURE so would end up stuck + // always log error, so we can look for it when manually testing with logging disabled + Log.e(TAG, "precapture start timeout"); + count_precapture_timeout++; + state = STATE_WAITING_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + } + } + else if( state == STATE_WAITING_PRECAPTURE_DONE ) { + if( MyDebug.LOG ) + Log.d(TAG, "waiting for precapture done..."); + if( MyDebug.LOG ) { + if( ae_state != null ) + Log.d(TAG, "CONTROL_AE_STATE = " + ae_state); + else + Log.d(TAG, "CONTROL_AE_STATE is null"); + } + if( ae_state == null || ae_state != CaptureResult.CONTROL_AE_STATE_PRECAPTURE ) { + if( MyDebug.LOG ) { + Log.d(TAG, "precapture completed after: " + (System.currentTimeMillis() - precapture_state_change_time_ms)); + } + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + takePictureAfterPrecapture(); + } + else if( precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > precapture_done_timeout_c ) { + // just in case + // always log error, so we can look for it when manually testing with logging disabled + Log.e(TAG, "precapture done timeout"); + count_precapture_timeout++; + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + takePictureAfterPrecapture(); + } + } + else if( state == STATE_WAITING_FAKE_PRECAPTURE_START ) { + if( MyDebug.LOG ) + Log.d(TAG, "waiting for fake precapture start..."); + if( MyDebug.LOG ) { + if( ae_state != null ) + Log.d(TAG, "CONTROL_AE_STATE = " + ae_state); + else + Log.d(TAG, "CONTROL_AE_STATE is null"); + } + if( fake_precapture_turn_on_torch_id != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "still waiting for torch to come on for fake precapture"); + } + + if( fake_precapture_turn_on_torch_id == null && (ae_state == null || ae_state == CaptureResult.CONTROL_AE_STATE_SEARCHING) ) { + if( MyDebug.LOG ) { + Log.d(TAG, "fake precapture started after: " + (System.currentTimeMillis() - precapture_state_change_time_ms)); + } + state = STATE_WAITING_FAKE_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + } + else if( precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > precapture_start_timeout_c ) { + // just in case + // always log error, so we can look for it when manually testing with logging disabled + Log.e(TAG, "fake precapture start timeout"); + count_precapture_timeout++; + state = STATE_WAITING_FAKE_PRECAPTURE_DONE; + precapture_state_change_time_ms = System.currentTimeMillis(); + fake_precapture_turn_on_torch_id = null; + } + } + else if( state == STATE_WAITING_FAKE_PRECAPTURE_DONE ) { + if( MyDebug.LOG ) + Log.d(TAG, "waiting for fake precapture done..."); + if( MyDebug.LOG ) { + if( ae_state != null ) + Log.d(TAG, "CONTROL_AE_STATE = " + ae_state); + else + Log.d(TAG, "CONTROL_AE_STATE is null"); + Log.d(TAG, "ready_for_capture? " + ready_for_capture); + } + // wait for af and ae scanning to end (need to check af too, as in continuous focus mode, a focus may start again after switching torch on for the fake precapture) + if( ready_for_capture && ( ae_state == null || ae_state != CaptureResult.CONTROL_AE_STATE_SEARCHING) ) { + if( MyDebug.LOG ) { + Log.d(TAG, "fake precapture completed after: " + (System.currentTimeMillis() - precapture_state_change_time_ms)); + } + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + takePictureAfterPrecapture(); + } + else if( precapture_state_change_time_ms != -1 && System.currentTimeMillis() - precapture_state_change_time_ms > precapture_done_timeout_c ) { + // sometimes camera can take a while to stop ae/af scanning, better to just go ahead and take photo + // always log error, so we can look for it when manually testing with logging disabled + Log.e(TAG, "fake precapture done timeout"); + count_precapture_timeout++; + state = STATE_NORMAL; + precapture_state_change_time_ms = -1; + takePictureAfterPrecapture(); + } + } + + if( af_state != null && af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN && af_state != last_af_state ) { + if( MyDebug.LOG ) + Log.d(TAG, "continuous focusing started"); + if( continuous_focus_move_callback != null ) { + continuous_focus_move_callback.onContinuousFocusMove(true); + } + } + else if( af_state != null && last_af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN && af_state != last_af_state ) { + if( MyDebug.LOG ) + Log.d(TAG, "continuous focusing stopped"); + if( continuous_focus_move_callback != null ) { + continuous_focus_move_callback.onContinuousFocusMove(false); + } + } + + if( af_state != null && af_state != last_af_state ) { + if( MyDebug.LOG ) + Log.d(TAG, "CONTROL_AF_STATE changed from " + last_af_state + " to " + af_state); + last_af_state = af_state; + } + } + + /** Processes a total result. + */ + private void processCompleted(CaptureRequest request, CaptureResult result) { + /*if( MyDebug.LOG ) + Log.d(TAG, "processCompleted");*/ + + if( result.get(CaptureResult.SENSOR_SENSITIVITY) != null ) { + capture_result_has_iso = true; + capture_result_iso = result.get(CaptureResult.SENSOR_SENSITIVITY); + /*if( MyDebug.LOG ) + Log.d(TAG, "capture_result_iso: " + capture_result_iso);*/ + if( camera_settings.has_iso && Math.abs(camera_settings.iso - capture_result_iso) > 10 ) { + // ugly hack: problem (on Nexus 6 at least) that when we start recording video (video_recorder.start() call), this often causes the ISO setting to reset to the wrong value! + // seems to happen more often with shorter exposure time + // seems to happen on other camera apps with Camera2 API too + // update: allow some tolerance, as on OnePlus 3T it's normal to have some slight difference between requested and actual + // this workaround still means a brief flash with incorrect ISO, but is best we can do for now! + if( MyDebug.LOG ) { + Log.d(TAG, "ISO " + capture_result_iso + " different to requested ISO " + camera_settings.iso); + Log.d(TAG, " requested ISO was: " + request.get(CaptureRequest.SENSOR_SENSITIVITY)); + Log.d(TAG, " requested AE mode was: " + request.get(CaptureRequest.CONTROL_AE_MODE)); + } + try { + setRepeatingRequest(); + } + catch(CameraAccessException e) { + if( MyDebug.LOG ) { + Log.e(TAG, "failed to set repeating request after ISO hack"); + Log.e(TAG, "reason: " + e.getReason()); + Log.e(TAG, "message: " + e.getMessage()); + } + e.printStackTrace(); + } + } + } + else { + capture_result_has_iso = false; + } + if( result.get(CaptureResult.SENSOR_EXPOSURE_TIME) != null ) { + capture_result_has_exposure_time = true; + capture_result_exposure_time = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); + } + else { + capture_result_has_exposure_time = false; + } + if( result.get(CaptureResult.SENSOR_FRAME_DURATION) != null ) { + capture_result_has_frame_duration = true; + capture_result_frame_duration = result.get(CaptureResult.SENSOR_FRAME_DURATION); + } + else { + capture_result_has_frame_duration = false; + } + /*if( MyDebug.LOG ) { + if( result.get(CaptureResult.SENSOR_EXPOSURE_TIME) != null ) { + long capture_result_exposure_time = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); + Log.d(TAG, "capture_result_exposure_time: " + capture_result_exposure_time); + } + if( result.get(CaptureResult.SENSOR_FRAME_DURATION) != null ) { + long capture_result_frame_duration = result.get(CaptureResult.SENSOR_FRAME_DURATION); + Log.d(TAG, "capture_result_frame_duration: " + capture_result_frame_duration); + } + }*/ + /*if( result.get(CaptureResult.LENS_FOCUS_RANGE) != null ) { + Pair focus_range = result.get(CaptureResult.LENS_FOCUS_RANGE); + capture_result_has_focus_distance = true; + capture_result_focus_distance_min = focus_range.first; + capture_result_focus_distance_max = focus_range.second; + } + else { + capture_result_has_focus_distance = false; + }*/ + + if( face_detection_listener != null && previewBuilder != null && previewBuilder.get(CaptureRequest.STATISTICS_FACE_DETECT_MODE) != null && previewBuilder.get(CaptureRequest.STATISTICS_FACE_DETECT_MODE) == CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) { + Rect sensor_rect = getViewableRect(); + android.hardware.camera2.params.Face [] camera_faces = result.get(CaptureResult.STATISTICS_FACES); + if( camera_faces != null ) { + CameraController.Face [] faces = new CameraController.Face[camera_faces.length]; + for(int i=0;i x_samples, List y_samples, List weights) { + if( MyDebug.LOG ) + Log.d(TAG, "ResponseFunction"); + + if( x_samples.size() != y_samples.size() ) { + if( MyDebug.LOG ) + Log.e(TAG, "unequal number of samples"); + // throw RuntimeException, as this is a programming error + throw new RuntimeException(); + } + else if( x_samples.size() != weights.size() ) { + if( MyDebug.LOG ) + Log.e(TAG, "unequal number of samples"); + // throw RuntimeException, as this is a programming error + throw new RuntimeException(); + } + else if( x_samples.size() <= 3 ) { + if( MyDebug.LOG ) + Log.e(TAG, "not enough samples"); + // throw RuntimeException, as this is a programming error + throw new RuntimeException(); + } + + // linear Y = AX + B + boolean done = false; + double sum_wx = 0.0; + double sum_wx2 = 0.0; + double sum_wxy = 0.0; + double sum_wy = 0.0; + double sum_w = 0.0; + for(int i=0;i bitmaps, boolean release_bitmaps, Bitmap output_bitmap, boolean assume_sorted) { + if( MyDebug.LOG ) + Log.d(TAG, "processHDR"); + int n_bitmaps = bitmaps.size(); + if( n_bitmaps != 1 && n_bitmaps != 3 ) { + if( MyDebug.LOG ) + Log.e(TAG, "n_bitmaps should be 1 or 3, not " + n_bitmaps); + // throw RuntimeException, as this is a programming error + throw new RuntimeException(); + } + for(int i=1;i x_samples = new ArrayList<>(); + List y_samples = new ArrayList<>(); + List weights = new ArrayList<>(); + + final int n_samples_c = 100; + final int n_w_samples = (int)Math.sqrt(n_samples_c); + final int n_h_samples = n_samples_c/n_w_samples; + + double avg_in = 0.0; + double avg_out = 0.0; + for(int y=0;y= in_bitmap.getWidth() || y_coord + offset_y < 0 || y_coord + offset_y >= in_bitmap.getHeight() ) { + continue; + } + int in_col = in_bitmap.getPixel(x_coord + offset_x, y_coord + offset_y); + int out_col = out_bitmap.getPixel(x_coord, y_coord); + double in_value = averageRGB(in_col); + double out_value = averageRGB(out_col); + avg_in += in_value; + avg_out += out_value; + x_samples.add(in_value); + y_samples.add(out_value); + } + } + if( x_samples.size() == 0 ) { + Log.e(TAG, "no samples for response function!"); + // shouldn't happen, but could do with a very large offset - just make up a dummy sample + double in_value = 255.0; + double out_value = 255.0; + avg_in += in_value; + avg_out += out_value; + x_samples.add(in_value); + y_samples.add(out_value); + } + avg_in /= x_samples.size(); + avg_out /= x_samples.size(); + boolean is_dark_exposure = avg_in < avg_out; + if( MyDebug.LOG ) { + Log.d(TAG, "avg_in: " + avg_in); + Log.d(TAG, "avg_out: " + avg_out); + Log.d(TAG, "is_dark_exposure: " + is_dark_exposure); + } + { + // calculate weights + double min_value = x_samples.get(0); + double max_value = x_samples.get(0); + for(int i=1;i max_value ) + max_value = value; + } + double med_value = 0.5*(min_value + max_value); + if( MyDebug.LOG ) { + Log.d(TAG, "min_value: " + min_value); + Log.d(TAG, "max_value: " + max_value); + Log.d(TAG, "med_value: " + med_value); + } + double min_value_y = y_samples.get(0); + double max_value_y = y_samples.get(0); + for(int i=1;i max_value_y ) + max_value_y = value; + } + double med_value_y = 0.5*(min_value_y + max_value_y); + if( MyDebug.LOG ) { + Log.d(TAG, "min_value_y: " + min_value_y); + Log.d(TAG, "max_value_y: " + max_value_y); + Log.d(TAG, "med_value_y: " + med_value_y); + } + for(int i=0;i> 16; + int g = (color & 0xFF00) >> 8; + int b = (color & 0xFF); + return (r + g + b)/3.0; + //return 0.27*r + 0.67*g + 0.06*b; + } + + /** Core implementation of HDR algorithm. + * Requires Android 4.4 (API level 19, Kitkat), due to using Renderscript without the support libraries. + * And we now need Android 5.0 (API level 21, Lollipop) for forEach_Dot with LaunchOptions. + * Using the support libraries (set via project.properties renderscript.support.mode) would bloat the APK + * by around 1799KB! We don't care about pre-Android 4.4 (HDR requires CameraController2 which requires + * Android 5.0 anyway; even if we later added support for CameraController1, we can simply say HDR requires + * Android 5.0). + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void processHDRCore(List bitmaps, boolean release_bitmaps, Bitmap output_bitmap, boolean assume_sorted) { + if( MyDebug.LOG ) + Log.d(TAG, "processHDRCore"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + if( MyDebug.LOG ) + Log.e(TAG, "HDR requires at least Android 5.0"); + // throw runtime exception as this is a programming error - HDR should not be offered as supported on older Android versions + // (we require Android 5 for Camera2 API, to offer burst mode, anyway) + throw new RuntimeException(); + } + long time_s = System.currentTimeMillis(); + + int n_bitmaps = bitmaps.size(); + int width = bitmaps.get(0).getWidth(); + int height = bitmaps.get(0).getHeight(); + ResponseFunction [] response_functions = new ResponseFunction[n_bitmaps]; // ResponseFunction for each image (the ResponseFunction entry can be left null to indicate the Identity) + /*int [][] buffers = new int[n_bitmaps][]; + for(int i=0;i bitmaps, boolean release_bitmaps, Bitmap output_bitmap) { + if (MyDebug.LOG) + Log.d(TAG, "processSingleImage"); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (MyDebug.LOG) + Log.e(TAG, "HDR requires at least Android 5.0"); + // throw runtime exception as this is a programming error - HDR should not be offered as supported on older Android versions + // (we require Android 5 for Camera2 API, to offer burst mode, anyway) + throw new RuntimeException(); + } + + long time_s = System.currentTimeMillis(); + + int width = bitmaps.get(0).getWidth(); + int height = bitmaps.get(0).getHeight(); + + initRenderscript(); + if (MyDebug.LOG) + Log.d(TAG, "### time after creating renderscript: " + (System.currentTimeMillis() - time_s)); + + // create allocation + Allocation allocation = Allocation.createFromBitmap(rs, bitmaps.get(0)); + + Allocation output_allocation; + if( release_bitmaps ) { + output_allocation = allocation; + } + else { + output_allocation = Allocation.createFromBitmap(rs, output_bitmap); + } + + adjustHistogram(allocation, output_allocation, width, height, time_s); + + if( release_bitmaps ) { + allocation.copyTo(bitmaps.get(0)); + if (MyDebug.LOG) + Log.d(TAG, "time after copying to bitmap: " + (System.currentTimeMillis() - time_s)); + } + else { + output_allocation.copyTo(output_bitmap); + if (MyDebug.LOG) + Log.d(TAG, "time after copying to bitmap: " + (System.currentTimeMillis() - time_s)); + } + + if( MyDebug.LOG ) + Log.d(TAG, "time for processSingleImage: " + (System.currentTimeMillis() - time_s)); + } + + private void initRenderscript() { + if (rs == null) { + // initialise renderscript + this.rs = RenderScript.create(context); + if (MyDebug.LOG) + Log.d(TAG, "create renderscript object"); + } + } + + /** + * If assume_sorted if false, this function will also sort the allocations and bitmaps from darkest to brightest. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void autoAlignment(int [] offsets_x, int [] offsets_y, Allocation [] allocations, int width, int height, List bitmaps, boolean assume_sorted, long time_s) { + if( MyDebug.LOG ) + Log.d(TAG, "autoAlignment"); + Allocation [] mtb_allocations = new Allocation[allocations.length]; + if( MyDebug.LOG ) + Log.d(TAG, "### time after creating mtb_allocations: " + (System.currentTimeMillis() - time_s)); + + // Testing shows that in practice we get good results by only aligning the centre quarter of the images. This gives better + // performance, and uses less memory. + int mtb_width = width/2; + int mtb_height = height/2; + int mtb_x = mtb_width/2; + int mtb_y = mtb_height/2; + /*int mtb_width = width; + int mtb_height = height; + int mtb_x = 0; + int mtb_y = 0;*/ + + // create RenderScript + ScriptC_create_mtb createMTBScript = new ScriptC_create_mtb(rs); + + LuminanceInfo [] luminanceInfos = new LuminanceInfo[allocations.length]; + for(int i=0;i bitmapInfos = new ArrayList<>(bitmaps.size()); + for(int i=0;i() { + @Override + public int compare(BitmapInfo o1, BitmapInfo o2) { + return o1.luminanceInfo.median_value - o2.luminanceInfo.median_value; + } + }); + bitmaps.clear(); + for(int i=0;i 1 ) { + step_size /= 2; + alignMTBScript.set_off_x( offsets_x[i] ); + alignMTBScript.set_off_y( offsets_y[i] ); + alignMTBScript.set_step_size( step_size ); + if( MyDebug.LOG ) { + Log.d(TAG, "call alignMTBScript for image: " + i); + Log.d(TAG, "step_size: " + step_size); + } + Allocation errorsAllocation = Allocation.createSized(rs, Element.I32(rs), 9); + alignMTBScript.bind_errors(errorsAllocation); + alignMTBScript.invoke_init_errors(); + + // see note inside align_mtb.rs/align_mtb() for why we sample over a subset of the image + Script.LaunchOptions launch_options = new Script.LaunchOptions(); + int stop_x = mtb_width/step_size; + int stop_y = mtb_height/step_size; + if( MyDebug.LOG ) { + Log.d(TAG, "stop_x: " + stop_x); + Log.d(TAG, "stop_y: " + stop_y); + } + //launch_options.setX((int)(stop_x*0.25), (int)(stop_x*0.75)); + //launch_options.setY((int)(stop_y*0.25), (int)(stop_y*0.75)); + launch_options.setX(0, stop_x); + launch_options.setY(0, stop_y); + //alignMTBScript.forEach_align_mtb(mtb_allocations[1]); + alignMTBScript.forEach_align_mtb(mtb_allocations[1], launch_options); + if( MyDebug.LOG ) + Log.d(TAG, "time after alignMTBScript: " + (System.currentTimeMillis() - time_s)); + + int best_error = -1; + int best_id = -1; + int [] errors = new int[9]; + errorsAllocation.copyTo(errors); + for(int j=0;j<9;j++) { + int this_error = errors[j]; + if( MyDebug.LOG ) + Log.d(TAG, " errors[" + j + "]: " + this_error); + if( best_id==-1 || this_error < best_error ) { + best_error = this_error; + best_id = j; + } + } + if( MyDebug.LOG ) + Log.d(TAG, " best_id " + best_id + " error: " + best_error); + if( best_id != -1 ) { + int this_off_x = best_id % 3; + int this_off_y = best_id/3; + this_off_x--; + this_off_y--; + if( MyDebug.LOG ) { + Log.d(TAG, "this_off_x: " + this_off_x); + Log.d(TAG, "this_off_y: " + this_off_y); + } + offsets_x[i] += this_off_x * step_size; + offsets_y[i] += this_off_y * step_size; + if( MyDebug.LOG ) { + Log.d(TAG, "offsets_x is now: " + offsets_x[i]); + Log.d(TAG, "offsets_y is now: " + offsets_y[i]); + } + } + } + } + + /*for(int i=0;i<3;i++) { + offsets_x[i] = 0; + offsets_y[i] = 0; + }*/ + } + + private static class LuminanceInfo { + final int median_value; + final boolean noisy; + + LuminanceInfo(int median_value, boolean noisy) { + this.median_value = median_value; + this.noisy = noisy; + } + } + + private LuminanceInfo computeMedianLuminance(Bitmap bitmap, int mtb_x, int mtb_y, int mtb_width, int mtb_height) { + if( MyDebug.LOG ) + Log.d(TAG, "computeMedianLuminance"); + final int n_samples_c = 100; + final int n_w_samples = (int)Math.sqrt(n_samples_c); + final int n_h_samples = n_samples_c/n_w_samples; + + int [] histo = new int[256]; + for(int i=0;i<256;i++) + histo[i] = 0; + int total = 0; + for(int y=0;y> 16; + int g = (color & 0xFF00) >> 8; + int b = (color & 0xFF); + int luminance = Math.max(r, g); + luminance = Math.max(luminance, b); + histo[luminance]++; + total++; + } + } + int middle = total/2; + int count = 0; + boolean noisy = false; + for(int i=0;i<256;i++) { + count += histo[i]; + if( count >= middle ) { + if( MyDebug.LOG ) + Log.d(TAG, "median luminance " + i); + final int noise_threshold = 4; + int n_below = 0, n_above = 0; + for(int j=0;j<=i-noise_threshold;j++) { + n_below += histo[j]; + } + for(int j=0;j<=i+noise_threshold && j<256;j++) { + n_above += histo[j]; + } + double frac_below = n_below / (double)total; + if( MyDebug.LOG ) { + double frac_above = 1.0 - n_above / (double)total; + Log.d(TAG, "count: " + count); + Log.d(TAG, "n_below: " + n_below); + Log.d(TAG, "n_above: " + n_above); + Log.d(TAG, "frac_below: " + frac_below); + Log.d(TAG, "frac_above: " + frac_above); + } + if( frac_below < 0.2 ) { + // needed for testHDR2, testHDR28 + // note that we don't exclude cases where frac_above is too small, as this could be an overexposed image - see testHDR31 + if( MyDebug.LOG ) + Log.d(TAG, "too dark/noisy"); + noisy = true; + } + return new LuminanceInfo(i, noisy); + } + } + Log.e(TAG, "computeMedianLuminance failed"); + return new LuminanceInfo(127, true); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void adjustHistogram(Allocation allocation_in, Allocation allocation_out, int width, int height, long time_s) { + if( MyDebug.LOG ) + Log.d(TAG, "adjustHistogram"); + final boolean adjust_histogram = false; + //final boolean adjust_histogram = true; + + if( adjust_histogram ) { + // create histogram + int [] histogram = new int[256]; + Allocation histogramAllocation = Allocation.createSized(rs, Element.I32(rs), 256); + //final boolean use_custom_histogram = false; + final boolean use_custom_histogram = true; + if( MyDebug.LOG ) + Log.d(TAG, "time before creating histogram: " + (System.currentTimeMillis() - time_s)); + if( use_custom_histogram ) { + if( MyDebug.LOG ) + Log.d(TAG, "create histogramScript"); + ScriptC_histogram_compute histogramScript = new ScriptC_histogram_compute(rs); + if( MyDebug.LOG ) + Log.d(TAG, "bind histogram allocation"); + histogramScript.bind_histogram(histogramAllocation); + histogramScript.invoke_init_histogram(); + if( MyDebug.LOG ) + Log.d(TAG, "call histogramScript"); + histogramScript.forEach_histogram_compute(allocation_in); + } + else { + ScriptIntrinsicHistogram histogramScript = ScriptIntrinsicHistogram.create(rs, Element.U8_4(rs)); + histogramScript.setOutput(histogramAllocation); + if( MyDebug.LOG ) + Log.d(TAG, "call histogramScript"); + histogramScript.forEach_Dot(allocation_in); // use forEach_dot(); using forEach would simply compute a histogram for red values! + } + if( MyDebug.LOG ) + Log.d(TAG, "time after creating histogram: " + (System.currentTimeMillis() - time_s)); + + //histogramAllocation.setAutoPadding(true); + histogramAllocation.copyTo(histogram); + + /*if( MyDebug.LOG ) { + // compare/adjust + allocations[0].copyTo(bm); + int [] debug_histogram = new int[256]; + for(int i=0;i<256;i++) { + debug_histogram[i] = 0; + } + int [] debug_buffer = new int[width]; + for(int y=0;y> 16); + float g = (float)((color & 0xFF00) >> 8); + float b = (float)(color & 0xFF); + //float value = 0.299f*r + 0.587f*g + 0.114f*b; // matches ScriptIntrinsicHistogram default behaviour + float value = Math.max(r, g); + value = Math.max(value, b); + int i_value = (int)value; + i_value = Math.min(255, i_value); // just in case + debug_histogram[i_value]++; + } + } + for(int x=0;x<256;x++) { + Log.d(TAG, "histogram[" + x + "] = " + histogram[x] + " debug_histogram: " + debug_histogram[x]); + //histogram[x] = debug_histogram[x]; + } + }*/ + + int [] c_histogram = new int[256]; + c_histogram[0] = histogram[0]; + for(int x=1;x<256;x++) { + c_histogram[x] = c_histogram[x-1] + histogram[x]; + } + /*if( MyDebug.LOG ) { + for(int x=0;x<256;x++) { + Log.d(TAG, "histogram[" + x + "] = " + histogram[x] + " cumulative: " + c_histogram[x]); + } + }*/ + histogramAllocation.copyFrom(c_histogram); + + ScriptC_histogram_adjust histogramAdjustScript = new ScriptC_histogram_adjust(rs); + histogramAdjustScript.set_c_histogram(histogramAllocation); + + if( MyDebug.LOG ) + Log.d(TAG, "call histogramAdjustScript"); + histogramAdjustScript.forEach_histogram_adjust(allocation_in, allocation_out); + if( MyDebug.LOG ) + Log.d(TAG, "time after histogramAdjustScript: " + (System.currentTimeMillis() - time_s)); + } + + //final boolean adjust_histogram_local = false; + final boolean adjust_histogram_local = true; + + if( adjust_histogram_local ) { + // Contrast Limited Adaptive Histogram Equalisation + // Note we don't fully equalise the histogram, rather the resultant image is the mid-point of the non-equalised and fully-equalised images + // See https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE + // Also see "Adaptive Histogram Equalization and its Variations" ( http://www.cs.unc.edu/Research/MIDAG/pubs/papers/Adaptive%20Histogram%20Equalization%20and%20Its%20Variations.pdf ), + // Pizer, Amburn, Austin, Cromartie, Geselowitz, Greer, ter Haar Romeny, Zimmerman, Zuiderveld (1987). + + // create histograms + Allocation histogramAllocation = Allocation.createSized(rs, Element.I32(rs), 256); + if( MyDebug.LOG ) + Log.d(TAG, "create histogramScript"); + ScriptC_histogram_compute histogramScript = new ScriptC_histogram_compute(rs); + if( MyDebug.LOG ) + Log.d(TAG, "bind histogram allocation"); + histogramScript.bind_histogram(histogramAllocation); + + //final int n_tiles_c = 8; + final int n_tiles_c = 4; + //final int n_tiles_c = 1; + int [] c_histogram = new int[n_tiles_c*n_tiles_c*256]; + for(int i=0;i> 16); + float g = (float)((color & 0xFF00) >> 8); + float b = (float)(color & 0xFF); + //float value = 0.299f*r + 0.587f*g + 0.114f*b; // matches ScriptIntrinsicHistogram default behaviour + float value = Math.max(r, g); + value = Math.max(value, b); + int i_value = (int)value; + i_value = Math.min(255, i_value); // just in case + debug_histogram[i_value]++; + } + } + for(int x=0;x<256;x++) { + Log.d(TAG, "histogram[" + x + "] = " + histogram[x] + " debug_histogram: " + debug_histogram[x]); + //histogram[x] = debug_histogram[x]; + } + }*/ + + // clip histogram, for Contrast Limited AHE algorithm + int n_pixels = (stop_x - start_x) * (stop_y - start_y); + int clip_limit = (5 * n_pixels) / 256; + /*if( MyDebug.LOG ) + Log.d(TAG, "clip_limit: " + clip_limit);*/ + { + // find real clip limit + int bottom = 0, top = clip_limit; + while( top - bottom > 1 ) { + int middle = (top + bottom)/2; + int sum = 0; + for(int x=0;x<256;x++) { + if( histogram[x] > middle ) { + sum += (histogram[x] - clip_limit); + } + } + if( sum > (clip_limit - middle) * 256 ) + top = middle; + else + bottom = middle; + } + clip_limit = (top + bottom)/2; + /*if( MyDebug.LOG ) + Log.d(TAG, "updated clip_limit: " + clip_limit);*/ + } + int n_clipped = 0; + for(int x=0;x<256;x++) { + if( histogram[x] > clip_limit ) { + n_clipped += (histogram[x] - clip_limit); + histogram[x] = clip_limit; + } + } + int n_clipped_per_bucket = n_clipped / 256; + /*if( MyDebug.LOG ) { + Log.d(TAG, "n_clipped: " + n_clipped); + Log.d(TAG, "n_clipped_per_bucket: " + n_clipped_per_bucket); + }*/ + for(int x=0;x<256;x++) { + histogram[x] += n_clipped_per_bucket; + } + + int histogram_offset = 256*(i*n_tiles_c+j); + c_histogram[histogram_offset] = histogram[0]; + for(int x=1;x<256;x++) { + c_histogram[histogram_offset+x] = c_histogram[histogram_offset+x-1] + histogram[x]; + } + /*if( MyDebug.LOG ) { + for(int x=0;x<256;x++) { + Log.d(TAG, "histogram[" + x + "] = " + histogram[x] + " cumulative: " + c_histogram[histogram_offset+x]); + } + }*/ + } + } + + if( MyDebug.LOG ) + Log.d(TAG, "time after creating histograms: " + (System.currentTimeMillis() - time_s)); + + Allocation c_histogramAllocation = Allocation.createSized(rs, Element.I32(rs), n_tiles_c*n_tiles_c*256); + c_histogramAllocation.copyFrom(c_histogram); + ScriptC_histogram_adjust histogramAdjustScript = new ScriptC_histogram_adjust(rs); + histogramAdjustScript.set_c_histogram(c_histogramAllocation); + histogramAdjustScript.set_n_tiles(n_tiles_c); + histogramAdjustScript.set_width(width); + histogramAdjustScript.set_height(height); + + if( MyDebug.LOG ) + Log.d(TAG, "call histogramAdjustScript"); + histogramAdjustScript.forEach_histogram_adjust(allocation_in, allocation_out); + if( MyDebug.LOG ) + Log.d(TAG, "time after histogramAdjustScript: " + (System.currentTimeMillis() - time_s)); + } + } +} diff --git a/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/src/main/java/net/sourceforge/opencamera/ImageSaver.java new file mode 100644 index 00000000..7ff99f37 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -0,0 +1,1738 @@ +package net.sourceforge.opencamera; + +import net.sourceforge.opencamera.CameraController.CameraController; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.hardware.camera2.DngCreator; +import android.location.Location; +import android.media.ExifInterface; +import android.media.Image; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +/** Handles the saving (and any required processing) of photos. + */ +public class ImageSaver extends Thread { + private static final String TAG = "ImageSaver"; + + private static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; + private static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + + private final Paint p = new Paint(); + + private final MainActivity main_activity; + private final HDRProcessor hdrProcessor; + + /* We use a separate count n_images_to_save, rather than just relying on the queue size, so we can take() an image from queue, + * but only decrement the count when we've finished saving the image. + * In general, n_images_to_save represents the number of images still to process, including ones currently being processed. + * Therefore we should always have n_images_to_save >= queue.size(). + */ + private int n_images_to_save = 0; + private final BlockingQueue queue = new ArrayBlockingQueue<>(1); // since we remove from the queue and then process in the saver thread, in practice the number of background photos - including the one being processed - is one more than the length of this queue + + private static class Request { + enum Type { + JPEG, + RAW, + DUMMY + } + Type type = Type.JPEG; + final boolean is_hdr; // for jpeg + final boolean save_expo; // for is_hdr + /* jpeg_images: for jpeg (may be null otherwise). + * If is_hdr==true, this should be 1 or 3 images, and the images are combined/converted to a HDR image (if there's only 1 + * image, this uses fake HDR or "DRO"). + * If is_hdr==false, then multiple images are saved sequentially. + */ + final List jpeg_images; + final DngCreator dngCreator; // for raw + final Image image; // for raw + final boolean image_capture_intent; + final Uri image_capture_intent_uri; + final boolean using_camera2; + final int image_quality; + final boolean do_auto_stabilise; + final double level_angle; + final boolean is_front_facing; + final boolean mirror; + final Date current_date; + final String preference_stamp; + final String preference_textstamp; + final int font_size; + final int color; + final String pref_style; + final String preference_stamp_dateformat; + final String preference_stamp_timeformat; + final String preference_stamp_gpsformat; + final boolean store_location; + final Location location; + final boolean store_geo_direction; + final double geo_direction; + int sample_factor = 1; // sampling factor for thumbnail, higher means lower quality + + Request(Type type, + boolean is_hdr, + boolean save_expo, + List jpeg_images, + DngCreator dngCreator, Image image, + boolean image_capture_intent, Uri image_capture_intent_uri, + boolean using_camera2, int image_quality, + boolean do_auto_stabilise, double level_angle, + boolean is_front_facing, + boolean mirror, + Date current_date, + String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, + boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + int sample_factor) { + this.type = type; + this.is_hdr = is_hdr; + this.save_expo = save_expo; + this.jpeg_images = jpeg_images; + this.dngCreator = dngCreator; + this.image = image; + this.image_capture_intent = image_capture_intent; + this.image_capture_intent_uri = image_capture_intent_uri; + this.using_camera2 = using_camera2; + this.image_quality = image_quality; + this.do_auto_stabilise = do_auto_stabilise; + this.level_angle = level_angle; + this.is_front_facing = is_front_facing; + this.mirror = mirror; + this.current_date = current_date; + this.preference_stamp = preference_stamp; + this.preference_textstamp = preference_textstamp; + this.font_size = font_size; + this.color = color; + this.pref_style = pref_style; + this.preference_stamp_dateformat = preference_stamp_dateformat; + this.preference_stamp_timeformat = preference_stamp_timeformat; + this.preference_stamp_gpsformat = preference_stamp_gpsformat; + this.store_location = store_location; + this.location = location; + this.store_geo_direction = store_geo_direction; + this.geo_direction = geo_direction; + this.sample_factor = sample_factor; + } + } + + ImageSaver(MainActivity main_activity) { + if( MyDebug.LOG ) + Log.d(TAG, "ImageSaver"); + this.main_activity = main_activity; + this.hdrProcessor = new HDRProcessor(main_activity); + + p.setAntiAlias(true); + } + + void onDestroy() { + if( MyDebug.LOG ) + Log.d(TAG, "onDestroy"); + if( hdrProcessor != null ) { + hdrProcessor.onDestroy(); + } + } + @Override + + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "starting ImageSaver thread..."); + while( true ) { + try { + if( MyDebug.LOG ) + Log.d(TAG, "ImageSaver thread reading from queue, size: " + queue.size()); + Request request = queue.take(); // if empty, take() blocks until non-empty + // Only decrement n_images_to_save after we've actually saved the image! Otherwise waitUntilDone() will return + // even though we still have a last image to be saved. + if( MyDebug.LOG ) + Log.d(TAG, "ImageSaver thread found new request from queue, size is now: " + queue.size()); + boolean success; + if( request.type == Request.Type.RAW ) { + if( MyDebug.LOG ) + Log.d(TAG, "request is raw"); + success = saveImageNowRaw(request.dngCreator, request.image, request.current_date); + } + else if( request.type == Request.Type.JPEG ) { + if( MyDebug.LOG ) + Log.d(TAG, "request is jpeg"); + success = saveImageNow(request); + } + else if( request.type == Request.Type.DUMMY ) { + if( MyDebug.LOG ) + Log.d(TAG, "request is dummy"); + success = true; + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "request is unknown type!"); + success = false; + } + if( MyDebug.LOG ) { + if( success ) + Log.d(TAG, "ImageSaver thread successfully saved image"); + else + Log.e(TAG, "ImageSaver thread failed to save image"); + } + synchronized( this ) { + n_images_to_save--; + if( MyDebug.LOG ) + Log.d(TAG, "ImageSaver thread processed new request from queue, images to save is now: " + n_images_to_save); + if( MyDebug.LOG && n_images_to_save < 0 ) { + Log.e(TAG, "images to save has become negative"); + throw new RuntimeException(); + } + notifyAll(); + } + } + catch(InterruptedException e) { + e.printStackTrace(); + if( MyDebug.LOG ) + Log.e(TAG, "interrupted while trying to read from ImageSaver queue"); + } + } + } + + /** Saves a photo. + * If do_in_background is true, the photo will be saved in a background thread. If the queue is full, the function will wait + * until it isn't full. Otherwise it will return immediately. The function always returns true for background saving. + * If do_in_background is false, the photo is saved on the current thread, and the function returns whether the photo was saved + * successfully. + */ + boolean saveImageJpeg(boolean do_in_background, + boolean is_hdr, + boolean save_expo, + List images, + boolean image_capture_intent, Uri image_capture_intent_uri, + boolean using_camera2, int image_quality, + boolean do_auto_stabilise, double level_angle, + boolean is_front_facing, + boolean mirror, + Date current_date, + String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, + boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + int sample_factor) { + if( MyDebug.LOG ) { + Log.d(TAG, "saveImageJpeg"); + Log.d(TAG, "do_in_background? " + do_in_background); + Log.d(TAG, "number of images: " + images.size()); + } + return saveImage(do_in_background, + false, + is_hdr, + save_expo, + images, + null, null, + image_capture_intent, image_capture_intent_uri, + using_camera2, image_quality, + do_auto_stabilise, level_angle, + is_front_facing, + mirror, + current_date, + preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, + store_location, location, store_geo_direction, geo_direction, + sample_factor); + } + + /** Saves a RAW photo. + * If do_in_background is true, the photo will be saved in a background thread. If the queue is full, the function will wait + * until it isn't full. Otherwise it will return immediately. The function always returns true for background saving. + * If do_in_background is false, the photo is saved on the current thread, and the function returns whether the photo was saved + * successfully. + */ + boolean saveImageRaw(boolean do_in_background, + DngCreator dngCreator, Image image, + Date current_date) { + if( MyDebug.LOG ) { + Log.d(TAG, "saveImageRaw"); + Log.d(TAG, "do_in_background? " + do_in_background); + } + return saveImage(do_in_background, + true, + false, + false, + null, + dngCreator, image, + false, null, + false, 0, + false, 0.0, + false, + false, + current_date, + null, null, 0, 0, null, null, null, null, + false, null, false, 0.0, + 1); + } + + /** Internal saveImage method to handle both JPEG and RAW. + */ + private boolean saveImage(boolean do_in_background, + boolean is_raw, + boolean is_hdr, + boolean save_expo, + List jpeg_images, + DngCreator dngCreator, Image image, + boolean image_capture_intent, Uri image_capture_intent_uri, + boolean using_camera2, int image_quality, + boolean do_auto_stabilise, double level_angle, + boolean is_front_facing, + boolean mirror, + Date current_date, + String preference_stamp, String preference_textstamp, int font_size, int color, String pref_style, String preference_stamp_dateformat, String preference_stamp_timeformat, String preference_stamp_gpsformat, + boolean store_location, Location location, boolean store_geo_direction, double geo_direction, + int sample_factor) { + if( MyDebug.LOG ) { + Log.d(TAG, "saveImage"); + Log.d(TAG, "do_in_background? " + do_in_background); + } + boolean success; + + //do_in_background = false; + + Request request = new Request(is_raw ? Request.Type.RAW : Request.Type.JPEG, + is_hdr, + save_expo, + jpeg_images, + dngCreator, image, + image_capture_intent, image_capture_intent_uri, + using_camera2, image_quality, + do_auto_stabilise, level_angle, + is_front_facing, + mirror, + current_date, + preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, + store_location, location, store_geo_direction, geo_direction, + sample_factor); + + if( do_in_background ) { + if( MyDebug.LOG ) + Log.d(TAG, "add background request"); + addRequest(request); + if( ( request.is_hdr && request.jpeg_images.size() > 1 ) || ( !is_raw && request.jpeg_images.size() > 1 ) ) { + // For (multi-image) HDR, we also add a dummy request, effectively giving it a cost of 2 - to reflect the fact that HDR is more memory intensive + // (arguably it should have a cost of 3, to reflect the 3 JPEGs, but one can consider this comparable to RAW+JPEG, which have a cost + // of 2, due to RAW and JPEG each needing their own request). + // Similarly for saving multiple images (expo-bracketing) + Request dummy_request = new Request(Request.Type.DUMMY, + false, + false, + null, + null, null, + false, null, + false, 0, + false, 0.0, + false, + false, + null, + null, null, 0, 0, null, null, null, null, + false, null, false, 0.0, + 1); + if( MyDebug.LOG ) + Log.d(TAG, "add dummy request"); + addRequest(dummy_request); + } + success = true; // always return true when done in background + } + else { + // wait for queue to be empty + waitUntilDone(); + if( is_raw ) { + success = saveImageNowRaw(request.dngCreator, request.image, request.current_date); + } + else { + success = saveImageNow(request); + } + } + + if( MyDebug.LOG ) + Log.d(TAG, "success: " + success); + return success; + } + + /** Adds a request to the background queue, blocking if the queue is already full + */ + private void addRequest(Request request) { + if( MyDebug.LOG ) + Log.d(TAG, "addRequest"); + // this should not be synchronized on "this": BlockingQueue is thread safe, and if it's blocking in queue.put(), we'll hang because + // the saver queue will need to synchronize on "this" in order to notifyAll() the main thread + boolean done = false; + while( !done ) { + try { + if( MyDebug.LOG ) + Log.d(TAG, "ImageSaver thread adding to queue, size: " + queue.size()); + synchronized( this ) { + // see above for why we don't synchronize the queue.put call + // but we synchronize modification to avoid risk of problems related to compiler optimisation (local caching or reordering) + // also see FindBugs warning due to inconsistent synchronisation + n_images_to_save++; // increment before adding to the queue, just to make sure the main thread doesn't think we're all done + } + queue.put(request); // if queue is full, put() blocks until it isn't full + if( MyDebug.LOG ) { + synchronized( this ) { // keep FindBugs happy + Log.d(TAG, "ImageSaver thread added to queue, size is now: " + queue.size()); + Log.d(TAG, "images still to save is now: " + n_images_to_save); + } + } + done = true; + } + catch(InterruptedException e) { + e.printStackTrace(); + if( MyDebug.LOG ) + Log.e(TAG, "interrupted while trying to add to ImageSaver queue"); + } + } + } + + /** Wait until the queue is empty and all pending images have been saved. + */ + void waitUntilDone() { + if( MyDebug.LOG ) + Log.d(TAG, "waitUntilDone"); + synchronized( this ) { + if( MyDebug.LOG ) { + Log.d(TAG, "queue is size " + queue.size()); + Log.d(TAG, "images still to save " + n_images_to_save); + } + while( n_images_to_save > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "wait until done..."); + try { + wait(); + } + catch(InterruptedException e) { + e.printStackTrace(); + if( MyDebug.LOG ) + Log.e(TAG, "interrupted while waiting for ImageSaver queue to be empty"); + } + if( MyDebug.LOG ) { + Log.d(TAG, "waitUntilDone: queue is size " + queue.size()); + Log.d(TAG, "waitUntilDone: images still to save " + n_images_to_save); + } + } + } + if( MyDebug.LOG ) + Log.d(TAG, "waitUntilDone: images all saved"); + } + + /** Loads a single jpeg as a Bitmaps. + * @param mutable Whether the bitmap should be mutable. Note that when converting to bitmaps + * for the image post-processing (auto-stabilise etc), in general we need the + * bitmap to be mutable (for photostamp to work). + */ + @SuppressWarnings("deprecation") + private Bitmap loadBitmap(byte [] jpeg_image, boolean mutable) { + if( MyDebug.LOG ) { + Log.d(TAG, "loadBitmap"); + Log.d(TAG, "mutable?: " + mutable); + } + BitmapFactory.Options options = new BitmapFactory.Options(); + if( MyDebug.LOG ) + Log.d(TAG, "options.inMutable is: " + options.inMutable); + options.inMutable = mutable; + if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ) { + // setting is ignored in Android 5 onwards + options.inPurgeable = true; + } + Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg_image, 0, jpeg_image.length, options); + if( bitmap == null ) { + Log.e(TAG, "failed to decode bitmap"); + } + return bitmap; + } + + /** Helper class for loadBitmaps(). + */ + private static class LoadBitmapThread extends Thread { + Bitmap bitmap; + final BitmapFactory.Options options; + final byte [] jpeg; + LoadBitmapThread(BitmapFactory.Options options, byte [] jpeg) { + this.options = options; + this.jpeg = jpeg; + } + + public void run() { + this.bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options); + } + } + + /** Converts the array of jpegs to Bitmaps. The bitmap with index mutable_id will be marked as mutable (or set to -1 to have no mutable bitmaps). + */ + @SuppressWarnings("deprecation") + private List loadBitmaps(List jpeg_images, int mutable_id) { + if( MyDebug.LOG ) { + Log.d(TAG, "loadBitmaps"); + Log.d(TAG, "mutable_id: " + mutable_id); + } + BitmapFactory.Options mutable_options = new BitmapFactory.Options(); + mutable_options.inMutable = true; // bitmap that needs to be writable + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = false; // later bitmaps don't need to be writable + if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ) { + // setting is ignored in Android 5 onwards + mutable_options.inPurgeable = true; + options.inPurgeable = true; + } + LoadBitmapThread [] threads = new LoadBitmapThread[jpeg_images.size()]; + for(int i=0;i bitmaps = new ArrayList<>(); + for(int i=0;i 1 && !request.image_capture_intent && request.save_expo ) { + if( MyDebug.LOG ) + Log.e(TAG, "save exposures"); + for(int i=0;i bitmaps = loadBitmaps(request.jpeg_images, base_bitmap); + if( bitmaps == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to load bitmaps"); + return false; + } + if( MyDebug.LOG ) { + Log.d(TAG, "HDR performance: time after decompressing base exposures: " + (System.currentTimeMillis() - time_s)); + } + if( MyDebug.LOG ) + Log.d(TAG, "before HDR first bitmap: " + bitmaps.get(0) + " is mutable? " + bitmaps.get(0).isMutable()); + hdrProcessor.processHDR(bitmaps, true, null, true); // this will recycle all the bitmaps except bitmaps.get(0), which will contain the hdr image + if( MyDebug.LOG ) { + Log.d(TAG, "HDR performance: time after creating HDR image: " + (System.currentTimeMillis() - time_s)); + } + if( MyDebug.LOG ) + Log.d(TAG, "after HDR first bitmap: " + bitmaps.get(0) + " is mutable? " + bitmaps.get(0).isMutable()); + Bitmap hdr_bitmap = bitmaps.get(0); + if( MyDebug.LOG ) + Log.d(TAG, "hdr_bitmap: " + hdr_bitmap + " is mutable? " + hdr_bitmap.isMutable()); + bitmaps.clear(); + System.gc(); + main_activity.savingImage(false); + + if( MyDebug.LOG ) + Log.d(TAG, "save HDR image"); + int base_image_id = ((request.jpeg_images.size()-1)/2); + if( MyDebug.LOG ) + Log.d(TAG, "base_image_id: " + base_image_id); + String suffix = request.jpeg_images.size() == 1 ? "_DRO" : "_HDR"; + success = saveSingleImageNow(request, request.jpeg_images.get(base_image_id), hdr_bitmap, suffix, true, true); + if( MyDebug.LOG && !success ) + Log.e(TAG, "saveSingleImageNow failed for hdr image"); + if( MyDebug.LOG ) { + Log.d(TAG, "HDR performance: time after saving HDR image: " + (System.currentTimeMillis() - time_s)); + } + hdr_bitmap.recycle(); + System.gc(); + } + else { + if( request.jpeg_images.size() > 1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "saveImageNow called with multiple images"); + int mid_image = request.jpeg_images.size()/2; + success = true; + for(int i=0;i 90 ) + level_angle -= 180; + if( MyDebug.LOG ) + Log.d(TAG, "auto stabilising... angle: " + level_angle); + if( bitmap == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "need to decode bitmap to auto-stabilise"); + // bitmap doesn't need to be mutable here, as this won't be the final bitmap retured from the auto-stabilise code + bitmap = loadBitmap(data, false); + if( bitmap == null ) { + main_activity.getPreview().showToast(null, R.string.failed_to_auto_stabilise); + System.gc(); + } + } + if( bitmap != null ) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if( MyDebug.LOG ) { + Log.d(TAG, "level_angle: " + level_angle); + Log.d(TAG, "decoded bitmap size " + width + ", " + height); + Log.d(TAG, "bitmap size: " + width*height*4); + } + /*for(int y=0;y= bitmap.getWidth() ) + w2 = bitmap.getWidth()-1; + if( h2 <= 0 ) + h2 = 1; + else if( h2 >= bitmap.getHeight() ) + h2 = bitmap.getHeight()-1; + int x0 = (bitmap.getWidth()-w2)/2; + int y0 = (bitmap.getHeight()-h2)/2; + if( MyDebug.LOG ) { + Log.d(TAG, "x0 = " + x0 + " , y0 = " + y0); + } + // We need the bitmap to be mutable for photostamp to work - contrary to the documentation for Bitmap.createBitmap + // (which says it returns an immutable bitmap), we seem to always get a mutable bitmap anyway. A mutable bitmap + // would result in an exception "java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor" + // from the Canvas(bitmap) constructor call in the photostamp code, and I've yet to see this from Google Play. + new_bitmap = Bitmap.createBitmap(bitmap, x0, y0, w2, h2); + if( new_bitmap != bitmap ) { + bitmap.recycle(); + bitmap = new_bitmap; + } + if( MyDebug.LOG ) + Log.d(TAG, "bitmap is mutable?: " + bitmap.isMutable()); + System.gc(); + } + } + return bitmap; + } + + /** Mirrors the image. + * @param data The jpeg data. + * @param bitmap Optional argument - the bitmap if already unpacked from the jpeg data. + * @return A bitmap representing the mirrored jpeg. + */ + private Bitmap mirrorImage(byte [] data, Bitmap bitmap) { + if( MyDebug.LOG ) { + Log.d(TAG, "mirrorImage"); + } + if( bitmap == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "need to decode bitmap to mirror"); + // bitmap doesn't need to be mutable here, as this won't be the final bitmap retured from the auto-stabilise code + bitmap = loadBitmap(data, false); + if( bitmap == null ) { + // don't bother warning to the user - we simply won't mirror the image + System.gc(); + } + } + if( bitmap != null ) { + Matrix matrix = new Matrix(); + matrix.preScale(-1.0f, 1.0f); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + Bitmap new_bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); + // careful, as new_bitmap is sometimes not a copy! + if( new_bitmap != bitmap ) { + bitmap.recycle(); + bitmap = new_bitmap; + } + if( MyDebug.LOG ) + Log.d(TAG, "bitmap is mutable?: " + bitmap.isMutable()); + } + return bitmap; + } + + /** Applies any photo stamp options (if they exist). + * @param data The jpeg data. + * @param bitmap Optional argument - the bitmap if already unpacked from the jpeg data. + * @return A bitmap representing the stamped jpeg. Will be null if the input bitmap is null and + * no photo stamp is applied. + */ + private Bitmap stampImage(final Request request, byte [] data, Bitmap bitmap) { + if( MyDebug.LOG ) { + Log.d(TAG, "stampImage"); + } + final MyApplicationInterface applicationInterface = main_activity.getApplicationInterface(); + boolean dategeo_stamp = request.preference_stamp.equals("preference_stamp_yes"); + boolean text_stamp = request.preference_textstamp.length() > 0; + if( dategeo_stamp || text_stamp ) { + if( bitmap == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "decode bitmap in order to stamp info"); + bitmap = loadBitmap(data, true); + if( bitmap == null ) { + main_activity.getPreview().showToast(null, R.string.failed_to_stamp); + System.gc(); + } + } + if( bitmap != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "stamp info to bitmap: " + bitmap); + if( MyDebug.LOG ) + Log.d(TAG, "bitmap is mutable?: " + bitmap.isMutable()); + int font_size = request.font_size; + int color = request.color; + String pref_style = request.pref_style; + String preference_stamp_dateformat = request.preference_stamp_dateformat; + String preference_stamp_timeformat = request.preference_stamp_timeformat; + String preference_stamp_gpsformat = request.preference_stamp_gpsformat; + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if( MyDebug.LOG ) { + Log.d(TAG, "decoded bitmap size " + width + ", " + height); + Log.d(TAG, "bitmap size: " + width*height*4); + } + Canvas canvas = new Canvas(bitmap); + p.setColor(Color.WHITE); + // we don't use the density of the screen, because we're stamping to the image, not drawing on the screen (we don't want the font height to depend on the device's resolution) + // instead we go by 1 pt == 1/72 inch height, and scale for an image height (or width if in portrait) of 4" (this means the font height is also independent of the photo resolution) + int smallest_size = (width 0 || time_stamp.length() > 0 ) { + String datetime_stamp = ""; + if( date_stamp.length() > 0 ) + datetime_stamp += date_stamp; + if( time_stamp.length() > 0 ) { + if( datetime_stamp.length() > 0 ) + datetime_stamp += " "; + datetime_stamp += time_stamp; + } + applicationInterface.drawTextWithBackground(canvas, p, datetime_stamp, color, Color.BLACK, width - offset_x, ypos, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, null, draw_shadowed); + } + ypos -= diff_y; + String gps_stamp = main_activity.getTextFormatter().getGPSString(preference_stamp_gpsformat, request.store_location, request.location, request.store_geo_direction, request.geo_direction); + if( gps_stamp.length() > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "stamp with location_string: " + gps_stamp); + applicationInterface.drawTextWithBackground(canvas, p, gps_stamp, color, Color.BLACK, width - offset_x, ypos, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, null, draw_shadowed); + ypos -= diff_y; + } + } + if( text_stamp ) { + if( MyDebug.LOG ) + Log.d(TAG, "stamp text"); + applicationInterface.drawTextWithBackground(canvas, p, request.preference_textstamp, color, Color.BLACK, width - offset_x, ypos, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, null, draw_shadowed); + ypos -= diff_y; + } + } + } + return bitmap; + } + + /** May be run in saver thread or picture callback thread (depending on whether running in background). + * The requests.images field is ignored, instead we save the supplied data or bitmap. + * If bitmap is null, then the supplied jpeg data is saved. If bitmap is non-null, then the bitmap is + * saved, but the supplied data is still used to read EXIF data from. + * @param update_thumbnail - Whether to update the thumbnail (and show the animation). + * @param share_image - Whether this image should be marked as the one to share (if multiple images can + * be saved from a single shot (e.g., saving exposure images with HDR). + */ + @SuppressLint("SimpleDateFormat") + @SuppressWarnings("deprecation") + private boolean saveSingleImageNow(final Request request, byte [] data, Bitmap bitmap, String filename_suffix, boolean update_thumbnail, boolean share_image) { + if( MyDebug.LOG ) + Log.d(TAG, "saveSingleImageNow"); + + if( request.type != Request.Type.JPEG ) { + if( MyDebug.LOG ) + Log.d(TAG, "saveImageNow called with non-jpeg request"); + // throw runtime exception, as this is a programming error + throw new RuntimeException(); + } + else if( data == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "saveSingleImageNow called with no data"); + // throw runtime exception, as this is a programming error + throw new RuntimeException(); + } + long time_s = System.currentTimeMillis(); + + // unpack: + final boolean image_capture_intent = request.image_capture_intent; + final boolean using_camera2 = request.using_camera2; + final Date current_date = request.current_date; + final boolean store_location = request.store_location; + final boolean store_geo_direction = request.store_geo_direction; + + boolean success = false; + final MyApplicationInterface applicationInterface = main_activity.getApplicationInterface(); + StorageUtils storageUtils = main_activity.getStorageUtils(); + + main_activity.savingImage(true); + + if( request.do_auto_stabilise ) { + bitmap = autoStabilise(data, bitmap, request.level_angle, request.is_front_facing); + } + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after auto-stabilise: " + (System.currentTimeMillis() - time_s)); + } + if( request.mirror ) { + bitmap = mirrorImage(data, bitmap); + } + bitmap = stampImage(request, data, bitmap); + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after photostamp: " + (System.currentTimeMillis() - time_s)); + } + + int exif_orientation_s = ExifInterface.ORIENTATION_UNDEFINED; + File picFile = null; + Uri saveUri = null; // if non-null, then picFile is a temporary file, which afterwards we should redirect to saveUri + try { + if( image_capture_intent ) { + if( MyDebug.LOG ) + Log.d(TAG, "image_capture_intent"); + if( request.image_capture_intent_uri != null ) + { + // Save the bitmap to the specified URI (use a try/catch block) + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + request.image_capture_intent_uri); + saveUri = request.image_capture_intent_uri; + } + else + { + // If the intent doesn't contain an URI, send the bitmap as a parcel + // (it is a good idea to reduce its size to ~50k pixels before) + if( MyDebug.LOG ) + Log.d(TAG, "sent to intent via parcel"); + if( bitmap == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "create bitmap"); + // bitmap we return doesn't need to be mutable + bitmap = loadBitmap(data, false); + } + if( bitmap != null ) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if( MyDebug.LOG ) { + Log.d(TAG, "decoded bitmap size " + width + ", " + height); + Log.d(TAG, "bitmap size: " + width*height*4); + } + final int small_size_c = 128; + if( width > small_size_c ) { + float scale = ((float)small_size_c)/(float)width; + if( MyDebug.LOG ) + Log.d(TAG, "scale to " + scale); + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + Bitmap new_bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); + // careful, as new_bitmap is sometimes not a copy! + if( new_bitmap != bitmap ) { + bitmap.recycle(); + bitmap = new_bitmap; + } + } + } + if( MyDebug.LOG ) { + if( bitmap != null ) { + Log.d(TAG, "returned bitmap size " + bitmap.getWidth() + ", " + bitmap.getHeight()); + Log.d(TAG, "returned bitmap size: " + bitmap.getWidth()*bitmap.getHeight()*4); + } + else { + Log.e(TAG, "no bitmap created"); + } + } + if( bitmap != null ) + main_activity.setResult(Activity.RESULT_OK, new Intent("inline-data").putExtra("data", bitmap)); + main_activity.finish(); + } + } + else if( storageUtils.isUsingSAF() ) { + saveUri = storageUtils.createOutputMediaFileSAF(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, "jpg", current_date); + } + else { + picFile = storageUtils.createOutputMediaFile(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, "jpg", current_date); + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + picFile.getAbsolutePath()); + } + + if( saveUri != null && picFile == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "saveUri: " + saveUri); + picFile = File.createTempFile("picFile", "jpg", main_activity.getCacheDir()); + if( MyDebug.LOG ) + Log.d(TAG, "temp picFile: " + picFile.getAbsolutePath()); + } + + if( picFile != null ) { + OutputStream outputStream = new FileOutputStream(picFile); + try { + if( bitmap != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "compress bitmap, quality " + request.image_quality); + bitmap.compress(Bitmap.CompressFormat.JPEG, request.image_quality, outputStream); + } + else { + outputStream.write(data); + } + } + finally { + outputStream.close(); + } + if( MyDebug.LOG ) + Log.d(TAG, "saveImageNow saved photo"); + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after saving photo: " + (System.currentTimeMillis() - time_s)); + } + + if( saveUri == null ) { // if saveUri is non-null, then we haven't succeeded until we've copied to the saveUri + success = true; + } + if( picFile != null ) { + if( bitmap != null ) { + // need to update EXIF data! + if( MyDebug.LOG ) + Log.d(TAG, "write temp file to record EXIF data"); + File tempFile = File.createTempFile("opencamera_exif", ""); + OutputStream tempOutputStream = new FileOutputStream(tempFile); + try { + tempOutputStream.write(data); + } + finally { + tempOutputStream.close(); + } + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after saving temp photo for EXIF: " + (System.currentTimeMillis() - time_s)); + } + exif_orientation_s = setExifFromFile(request, tempFile, picFile); + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after copying EXIF: " + (System.currentTimeMillis() - time_s)); + } + if( !tempFile.delete() ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to delete temp " + tempFile.getAbsolutePath()); + } + if( MyDebug.LOG ) + Log.d(TAG, "now saved EXIF data"); + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after writing EXIF: " + (System.currentTimeMillis() - time_s)); + } + } + else if( store_geo_direction ) { + if( MyDebug.LOG ) + Log.d(TAG, "add GPS direction exif info"); + try { + ExifInterface exif = new ExifInterface(picFile.getAbsolutePath()); + setGPSDirectionExif(exif, store_geo_direction, request.geo_direction); + setDateTimeExif(exif); + if( needGPSTimestampHack(using_camera2, store_location) ) { + fixGPSTimestamp(exif, current_date); + } + exif.saveAttributes(); + } + catch(NoClassDefFoundError exception) { + // have had Google Play crashes from new ExifInterface() elsewhere for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn), so also catch here just in case + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation NoClassDefFoundError"); + exception.printStackTrace(); + } + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after adding GPS direction exif info: " + (System.currentTimeMillis() - time_s)); + } + } + else if( needGPSTimestampHack(using_camera2, store_location) ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove GPS timestamp hack"); + try { + ExifInterface exif = new ExifInterface(picFile.getAbsolutePath()); + fixGPSTimestamp(exif, current_date); + exif.saveAttributes(); + } + catch(NoClassDefFoundError exception) { + // have had Google Play crashes from new ExifInterface() elsewhere for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn), so also catch here just in case + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation NoClassDefFoundError"); + exception.printStackTrace(); + } + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after removing GPS timestamp hack: " + (System.currentTimeMillis() - time_s)); + } + } + + if( saveUri == null ) { + // broadcast for SAF is done later, when we've actually written out the file + storageUtils.broadcastFile(picFile, true, false, update_thumbnail); + main_activity.test_last_saved_image = picFile.getAbsolutePath(); + } + } + if( image_capture_intent ) { + if( MyDebug.LOG ) + Log.d(TAG, "finish activity due to being called from intent"); + main_activity.setResult(Activity.RESULT_OK); + main_activity.finish(); + } + if( storageUtils.isUsingSAF() ) { + // most Gallery apps don't seem to recognise the SAF-format Uri, so just clear the field + storageUtils.clearLastMediaScanned(); + } + + if( saveUri != null ) { + copyFileToUri(main_activity, saveUri, picFile); + success = true; + /* We still need to broadcastFile for SAF for two reasons: + 1. To call storageUtils.announceUri() to broadcast NEW_PICTURE etc. + Whilst in theory we could do this directly, it seems external apps that use such broadcasts typically + won't know what to do with a SAF based Uri (e.g, Owncloud crashes!) so better to broadcast the Uri + corresponding to the real file, if it exists. + 2. Whilst the new file seems to be known by external apps such as Gallery without having to call media + scanner, I've had reports this doesn't happen when saving to external SD cards. So better to explicitly + scan. + */ + File real_file = storageUtils.getFileFromDocumentUriSAF(saveUri, false); + if( MyDebug.LOG ) + Log.d(TAG, "real_file: " + real_file); + if( real_file != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "broadcast file"); + storageUtils.broadcastFile(real_file, true, false, true); + main_activity.test_last_saved_image = real_file.getAbsolutePath(); + } + else if( !image_capture_intent ) { + if( MyDebug.LOG ) + Log.d(TAG, "announce SAF uri"); + // announce the SAF Uri + // (shouldn't do this for a capture intent - e.g., causes crash when calling from Google Keep) + storageUtils.announceUri(saveUri, true, false); + } + } + } + } + catch(FileNotFoundException e) { + if( MyDebug.LOG ) + Log.e(TAG, "File not found: " + e.getMessage()); + e.printStackTrace(); + main_activity.getPreview().showToast(null, R.string.failed_to_save_photo); + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "I/O error writing file: " + e.getMessage()); + e.printStackTrace(); + main_activity.getPreview().showToast(null, R.string.failed_to_save_photo); + } + + if( success && saveUri == null ) { + applicationInterface.addLastImage(picFile, share_image); + } + else if( success && storageUtils.isUsingSAF() ){ + applicationInterface.addLastImageSAF(saveUri, share_image); + } + + // I have received crashes where camera_controller was null - could perhaps happen if this thread was running just as the camera is closing? + if( success && main_activity.getPreview().getCameraController() != null && update_thumbnail ) { + // update thumbnail - this should be done after restarting preview, so that the preview is started asap + CameraController.Size size = main_activity.getPreview().getCameraController().getPictureSize(); + int ratio = (int) Math.ceil((double) size.width / main_activity.getPreview().getView().getWidth()); + int sample_size = Integer.highestOneBit(ratio); + sample_size *= request.sample_factor; + if( MyDebug.LOG ) { + Log.d(TAG, " picture width: " + size.width); + Log.d(TAG, " preview width: " + main_activity.getPreview().getView().getWidth()); + Log.d(TAG, " ratio : " + ratio); + Log.d(TAG, " sample_size : " + sample_size); + } + Bitmap thumbnail; + if( bitmap == null ) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = false; + if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ) { + // setting is ignored in Android 5 onwards + options.inPurgeable = true; + } + options.inSampleSize = sample_size; + thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length, options); + } + else { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + Matrix matrix = new Matrix(); + float scale = 1.0f / (float)sample_size; + matrix.postScale(scale, scale); + if( MyDebug.LOG ) + Log.d(TAG, " scale: " + scale); + thumbnail = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); + } + if( thumbnail == null ) { + // received crashes on Google Play suggesting that thumbnail could not be created + if( MyDebug.LOG ) + Log.e(TAG, "failed to create thumbnail bitmap"); + } + else { + // now get the rotation from the Exif data + thumbnail = rotateForExif(thumbnail, exif_orientation_s, picFile.getAbsolutePath()); + + final Bitmap thumbnail_f = thumbnail; + main_activity.runOnUiThread(new Runnable() { + public void run() { + applicationInterface.updateThumbnail(thumbnail_f); + } + }); + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: time after creating thumbnail: " + (System.currentTimeMillis() - time_s)); + } + } + } + + if( bitmap != null ) { + bitmap.recycle(); + } + + if( picFile != null && saveUri != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "delete temp picFile: " + picFile); + if( !picFile.delete() ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to delete temp picFile: " + picFile); + } + } + + System.gc(); + + main_activity.savingImage(false); + + if( MyDebug.LOG ) { + Log.d(TAG, "Save single image performance: total time: " + (System.currentTimeMillis() - time_s)); + } + return success; + } + + @SuppressWarnings("deprecation") + private int setExifFromFile(final Request request, File from_file, File to_file) throws IOException { + if( MyDebug.LOG ) + Log.d(TAG, "setExifFromFile"); + int exif_orientation_s = ExifInterface.ORIENTATION_UNDEFINED; + if( MyDebug.LOG ) + Log.d(TAG, "read back EXIF data"); + try { + ExifInterface exif = new ExifInterface(from_file.getAbsolutePath()); + String exif_aperture = exif.getAttribute(ExifInterface.TAG_APERTURE); + String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME); + String exif_exposure_time = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); + String exif_flash = exif.getAttribute(ExifInterface.TAG_FLASH); + String exif_focal_length = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH); + String exif_gps_altitude = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE); + String exif_gps_altitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF); + String exif_gps_datestamp = exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP); + String exif_gps_latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); + String exif_gps_latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); + String exif_gps_longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); + String exif_gps_longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + String exif_gps_processing_method = exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); + String exif_gps_timestamp = exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP); + // leave width/height, as this may have changed! + String exif_iso = exif.getAttribute(ExifInterface.TAG_ISO); + String exif_make = exif.getAttribute(ExifInterface.TAG_MAKE); + String exif_model = exif.getAttribute(ExifInterface.TAG_MODEL); + int exif_orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + exif_orientation_s = exif_orientation; // store for later use (for the thumbnail, to save rereading it) + String exif_white_balance = exif.getAttribute(ExifInterface.TAG_WHITE_BALANCE); + + if( MyDebug.LOG ) + Log.d(TAG, "now write new EXIF data"); + ExifInterface exif_new = new ExifInterface(to_file.getAbsolutePath()); + if( exif_aperture != null ) + exif_new.setAttribute(ExifInterface.TAG_APERTURE, exif_aperture); + if( exif_datetime != null ) + exif_new.setAttribute(ExifInterface.TAG_DATETIME, exif_datetime); + if( exif_exposure_time != null ) + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, exif_exposure_time); + if( exif_flash != null ) + exif_new.setAttribute(ExifInterface.TAG_FLASH, exif_flash); + if( exif_focal_length != null ) + exif_new.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, exif_focal_length); + if( exif_gps_altitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, exif_gps_altitude); + if( exif_gps_altitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, exif_gps_altitude_ref); + if( exif_gps_datestamp != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, exif_gps_datestamp); + if( exif_gps_latitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE, exif_gps_latitude); + if( exif_gps_latitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, exif_gps_latitude_ref); + if( exif_gps_longitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, exif_gps_longitude); + if( exif_gps_longitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, exif_gps_longitude_ref); + if( exif_gps_processing_method != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, exif_gps_processing_method); + if( exif_gps_timestamp != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, exif_gps_timestamp); + // leave width/height, as this may have changed! + if( exif_iso != null ) + exif_new.setAttribute(ExifInterface.TAG_ISO, exif_iso); + if( exif_make != null ) + exif_new.setAttribute(ExifInterface.TAG_MAKE, exif_make); + if( exif_model != null ) + exif_new.setAttribute(ExifInterface.TAG_MODEL, exif_model); + if( exif_orientation != ExifInterface.ORIENTATION_UNDEFINED ) + exif_new.setAttribute(ExifInterface.TAG_ORIENTATION, "" + exif_orientation); + if( exif_white_balance != null ) + exif_new.setAttribute(ExifInterface.TAG_WHITE_BALANCE, exif_white_balance); + setGPSDirectionExif(exif_new, request.store_geo_direction, request.geo_direction); + setDateTimeExif(exif_new); + if( needGPSTimestampHack(request.using_camera2, request.store_location) ) { + fixGPSTimestamp(exif_new, request.current_date); + } + exif_new.saveAttributes(); + } + catch(NoClassDefFoundError exception) { + // have had Google Play crashes from new ExifInterface() for Galaxy Ace4 (vivalto3g) + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation NoClassDefFoundError"); + exception.printStackTrace(); + } + return exif_orientation_s; + } + + /** May be run in saver thread or picture callback thread (depending on whether running in background). + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private boolean saveImageNowRaw(DngCreator dngCreator, Image image, Date current_date) { + if( MyDebug.LOG ) + Log.d(TAG, "saveImageNowRaw"); + + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) { + if( MyDebug.LOG ) + Log.e(TAG, "RAW requires LOLLIPOP or higher"); + return false; + } + StorageUtils storageUtils = main_activity.getStorageUtils(); + boolean success = false; + + main_activity.savingImage(true); + + OutputStream output = null; + try { + File picFile = null; + Uri saveUri = null; + + if( storageUtils.isUsingSAF() ) { + saveUri = storageUtils.createOutputMediaFileSAF(StorageUtils.MEDIA_TYPE_IMAGE, "", "dng", current_date); + if( MyDebug.LOG ) + Log.d(TAG, "saveUri: " + saveUri); + // When using SAF, we don't save to a temp file first (unlike for JPEGs). Firstly we don't need to modify Exif, so don't + // need a real file; secondly copying to a temp file is much slower for RAW. + } + else { + picFile = storageUtils.createOutputMediaFile(StorageUtils.MEDIA_TYPE_IMAGE, "", "dng", current_date); + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + picFile.getAbsolutePath()); + } + + if( picFile != null ) { + output = new FileOutputStream(picFile); + } + else { + output = main_activity.getContentResolver().openOutputStream(saveUri); + } + dngCreator.writeImage(output, image); + image.close(); + image = null; + dngCreator.close(); + dngCreator = null; + output.close(); + output = null; + + /*Location location = null; + if( main_activity.getApplicationInterface().getGeotaggingPref() ) { + location = main_activity.getApplicationInterface().getLocation(); + if( MyDebug.LOG ) + Log.d(TAG, "location: " + location); + }*/ + + if( saveUri == null ) { + success = true; + //Uri media_uri = storageUtils.broadcastFileRaw(picFile, current_date, location); + //storageUtils.announceUri(media_uri, true, false); + storageUtils.broadcastFile(picFile, true, false, false); + } + else { + success = true; + File real_file = storageUtils.getFileFromDocumentUriSAF(saveUri, false); + if( MyDebug.LOG ) + Log.d(TAG, "real_file: " + real_file); + if( real_file != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "broadcast file"); + //Uri media_uri = storageUtils.broadcastFileRaw(real_file, current_date, location); + //storageUtils.announceUri(media_uri, true, false); + storageUtils.broadcastFile(real_file, true, false, false); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "announce SAF uri"); + storageUtils.announceUri(saveUri, true, false); + } + } + + MyApplicationInterface applicationInterface = main_activity.getApplicationInterface(); + if( success && saveUri == null ) { + applicationInterface.addLastImage(picFile, false); + } + else if( success && storageUtils.isUsingSAF() ){ + applicationInterface.addLastImageSAF(saveUri, false); + } + + } + catch(FileNotFoundException e) { + if( MyDebug.LOG ) + Log.e(TAG, "File not found: " + e.getMessage()); + e.printStackTrace(); + main_activity.getPreview().showToast(null, R.string.failed_to_save_photo_raw); + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "ioexception writing raw image file"); + e.printStackTrace(); + main_activity.getPreview().showToast(null, R.string.failed_to_save_photo_raw); + } + finally { + if( output != null ) { + try { + output.close(); + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "ioexception closing raw output"); + e.printStackTrace(); + } + } + if( image != null ) { + image.close(); + } + if( dngCreator != null ) { + dngCreator.close(); + } + } + + + System.gc(); + + main_activity.savingImage(false); + + return success; + } + + private Bitmap rotateForExif(Bitmap bitmap, int exif_orientation_s, String path) { + try { + if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED ) { + // haven't already read the exif orientation (or it didn't exist?) + if( MyDebug.LOG ) + Log.d(TAG, " read exif orientation"); + ExifInterface exif = new ExifInterface(path); + exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + } + if( MyDebug.LOG ) + Log.d(TAG, " exif orientation string: " + exif_orientation_s); + boolean needs_tf = false; + int exif_orientation = 0; + // from http://jpegclub.org/exif_orientation.html + // and http://stackoverflow.com/questions/20478765/how-to-get-the-correct-orientation-of-the-image-selected-from-the-default-image + if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED || exif_orientation_s == ExifInterface.ORIENTATION_NORMAL ) { + // leave unchanged + } + else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_180 ) { + needs_tf = true; + exif_orientation = 180; + } + else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_90 ) { + needs_tf = true; + exif_orientation = 90; + } + else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_270 ) { + needs_tf = true; + exif_orientation = 270; + } + else { + // just leave unchanged for now + if( MyDebug.LOG ) + Log.e(TAG, " unsupported exif orientation: " + exif_orientation_s); + } + if( MyDebug.LOG ) + Log.d(TAG, " exif orientation: " + exif_orientation); + + if( needs_tf ) { + Matrix m = new Matrix(); + m.setRotate(exif_orientation, bitmap.getWidth() * 0.5f, bitmap.getHeight() * 0.5f); + Bitmap rotated_bitmap = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(), bitmap.getHeight(), m, true); + if( rotated_bitmap != bitmap ) { + bitmap.recycle(); + bitmap = rotated_bitmap; + } + } + } + catch(IOException exception) { + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation ioexception"); + exception.printStackTrace(); + } + catch(NoClassDefFoundError exception) { + // have had Google Play crashes from new ExifInterface() for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn) + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation NoClassDefFoundError"); + exception.printStackTrace(); + } + return bitmap; + } + + private void setGPSDirectionExif(ExifInterface exif, boolean store_geo_direction, double geo_direction) { + if( store_geo_direction ) { + float geo_angle = (float)Math.toDegrees(geo_direction); + if( geo_angle < 0.0f ) { + geo_angle += 360.0f; + } + if( MyDebug.LOG ) + Log.d(TAG, "save geo_angle: " + geo_angle); + // see http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html + String GPSImgDirection_string = Math.round(geo_angle*100) + "/100"; + if( MyDebug.LOG ) + Log.d(TAG, "GPSImgDirection_string: " + GPSImgDirection_string); + exif.setAttribute(TAG_GPS_IMG_DIRECTION, GPSImgDirection_string); + exif.setAttribute(TAG_GPS_IMG_DIRECTION_REF, "M"); + } + } + + private void setDateTimeExif(ExifInterface exif) { + String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME); + if( exif_datetime != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "write datetime tags: " + exif_datetime); + exif.setAttribute("DateTimeOriginal", exif_datetime); + exif.setAttribute("DateTimeDigitized", exif_datetime); + } + } + + private void fixGPSTimestamp(ExifInterface exif, Date current_date) { + if( MyDebug.LOG ) { + Log.d(TAG, "fixGPSTimestamp"); + Log.d(TAG, "current datestamp: " + exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP)); + Log.d(TAG, "current timestamp: " + exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP)); + Log.d(TAG, "current datetime: " + exif.getAttribute(ExifInterface.TAG_DATETIME)); + } + // Hack: Problem on Camera2 API (at least on Nexus 6) that if geotagging is enabled, then the resultant image has incorrect Exif TAG_GPS_DATESTAMP and TAG_GPS_TIMESTAMP (GPSDateStamp) set (date tends to be around 2038 - possibly a driver bug of casting long to int?). + // This causes problems when viewing with Gallery apps, as they show this incorrect date. + // Update: Before v1.34 this was "fixed" by calling: exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, Long.toString(System.currentTimeMillis())); + // However this stopped working on or before 20161006. This wasn't a change in Open Camera (whilst this was working fine in + // 1.33 when I released it, the bug had come back when I retested that version) and I'm not sure how this ever worked, since + // TAG_GPS_TIMESTAMP is meant to be a string such "21:45:23", and not the number of ms since 1970 - possibly it wasn't really + // working , and was simply invalidating it such that Gallery then fell back to looking elsewhere for the datetime? + // So now hopefully fixed properly... + SimpleDateFormat date_fmt = new SimpleDateFormat("yyyy:MM:dd", Locale.US); + date_fmt.setTimeZone(TimeZone.getTimeZone("UTC")); // needs to be UTC time + String datestamp = date_fmt.format(current_date); + + SimpleDateFormat time_fmt = new SimpleDateFormat("HH:mm:ss", Locale.US); + time_fmt.setTimeZone(TimeZone.getTimeZone("UTC")); + String timestamp = time_fmt.format(current_date); + + if( MyDebug.LOG ) { + Log.d(TAG, "datestamp: " + datestamp); + Log.d(TAG, "timestamp: " + timestamp); + } + exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, datestamp); + exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, timestamp); + + if( MyDebug.LOG ) + Log.d(TAG, "fixGPSTimestamp exit"); + } + + private boolean needGPSTimestampHack(boolean using_camera2, boolean store_location) { + if( using_camera2 ) { + return store_location; + } + return false; + } + + /** Reads from picFile and writes the contents to saveUri. + */ + private void copyFileToUri(Context context, Uri saveUri, File picFile) throws IOException { + if( MyDebug.LOG ) { + Log.d(TAG, "copyFileToUri"); + Log.d(TAG, "saveUri: " + saveUri); + Log.d(TAG, "picFile: " + saveUri); + } + InputStream inputStream = null; + OutputStream realOutputStream = null; + try { + inputStream = new FileInputStream(picFile); + realOutputStream = context.getContentResolver().openOutputStream(saveUri); + // Transfer bytes from in to out + byte [] buffer = new byte[1024]; + int len; + while( (len = inputStream.read(buffer)) > 0 ) { + realOutputStream.write(buffer, 0, len); + } + } + finally { + if( inputStream != null ) { + inputStream.close(); + } + if( realOutputStream != null ) { + realOutputStream.close(); + } + } + } + + // for testing: + + HDRProcessor getHDRProcessor() { + return hdrProcessor; + } +} diff --git a/src/main/java/net/sourceforge/opencamera/LocationSupplier.java b/src/main/java/net/sourceforge/opencamera/LocationSupplier.java new file mode 100644 index 00000000..699306c6 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/LocationSupplier.java @@ -0,0 +1,242 @@ +package net.sourceforge.opencamera; + +import android.Manifest; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.util.Log; + +/** Handles listening for GPS location (both coarse and fine). + */ +public class LocationSupplier { + private static final String TAG = "LocationSupplier"; + + private final Context context; + private final LocationManager locationManager; + private MyLocationListener [] locationListeners; + + LocationSupplier(Context context) { + this.context = context; + locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE); + } + + public Location getLocation() { + // returns null if not available + if( locationListeners == null ) + return null; + // location listeners should be stored in order best to worst + for(MyLocationListener locationListener : locationListeners) { + Location location = locationListener.getLocation(); + if( location != null ) + return location; + } + return null; + } + + private static class MyLocationListener implements LocationListener { + private Location location; + volatile boolean test_has_received_location; // must be volatile for test project reading the state + + Location getLocation() { + return location; + } + + public void onLocationChanged(Location location) { + if( MyDebug.LOG ) + Log.d(TAG, "onLocationChanged"); + this.test_has_received_location = true; + // Android camera source claims we need to check lat/long != 0.0d + if( location.getLatitude() != 0.0d || location.getLongitude() != 0.0d ) { + if( MyDebug.LOG ) { + Log.d(TAG, "received location:"); + Log.d(TAG, "lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy()); + } + this.location = location; + } + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + switch( status ) { + case LocationProvider.OUT_OF_SERVICE: + case LocationProvider.TEMPORARILY_UNAVAILABLE: + { + if( MyDebug.LOG ) { + if( status == LocationProvider.OUT_OF_SERVICE ) + Log.d(TAG, "location provider out of service"); + else if( status == LocationProvider.TEMPORARILY_UNAVAILABLE ) + Log.d(TAG, "location provider temporarily unavailable"); + } + this.location = null; + this.test_has_received_location = false; + break; + } + default: + break; + } + } + + public void onProviderEnabled(String provider) { + } + + public void onProviderDisabled(String provider) { + if( MyDebug.LOG ) + Log.d(TAG, "onProviderDisabled"); + this.location = null; + this.test_has_received_location = false; + } + } + + // returns false if location permission not available for either coarse or fine + boolean setupLocationListener() { + if( MyDebug.LOG ) + Log.d(TAG, "setupLocationListener"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + // Define a listener that responds to location updates + // we only set it up if store_location is true, to avoid unnecessarily wasting battery + boolean store_location = sharedPreferences.getBoolean(PreferenceKeys.getLocationPreferenceKey(), false); + if( store_location && locationListeners == null ) { + // Note, ContextCompat.checkSelfPermission is meant to handle being called on any Android version, i.e., pre + // Android Marshmallow it should return true as permissions are set an installation, and can't be switched off by + // the user. However on Galaxy Nexus Android 4.3 and Nexus 7 (2013) Android 5.1.1, ACCESS_COARSE_LOCATION returns + // PERMISSION_DENIED! So we keep the checks to Android Marshmallow or later (where we need them), and avoid + // checking behaviour for earlier devices. + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.d(TAG, "check for location permissions"); + boolean has_coarse_location_permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; + boolean has_fine_location_permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + if( MyDebug.LOG ) { + Log.d(TAG, "has_coarse_location_permission? " + has_coarse_location_permission); + Log.d(TAG, "has_fine_location_permission? " + has_fine_location_permission); + } + // require both permissions to be present + if( !has_coarse_location_permission || !has_fine_location_permission ) { + if( MyDebug.LOG ) + Log.d(TAG, "location permission not available"); + // return false, which tells caller to request permission - we'll call this function again if permission is granted + return false; + } + } + + locationListeners = new MyLocationListener[2]; + locationListeners[0] = new MyLocationListener(); + locationListeners[1] = new MyLocationListener(); + + // location listeners should be stored in order best to worst + // also see https://sourceforge.net/p/opencamera/tickets/1/ - need to check provider is available + // now also need to check for permissions - need to support devices that might have one but not both of fine and coarse permissions supplied + if( locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER) ) { + locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 0, locationListeners[1]); + if( MyDebug.LOG ) + Log.d(TAG, "created coarse (network) location listener"); + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "don't have a NETWORK_PROVIDER"); + } + if( locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER) ) { + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0, locationListeners[0]); + if( MyDebug.LOG ) + Log.d(TAG, "created fine (gps) location listener"); + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "don't have a GPS_PROVIDER"); + } + } + else if( !store_location ) { + freeLocationListeners(); + } + return true; + } + + void freeLocationListeners() { + if( MyDebug.LOG ) + Log.d(TAG, "freeLocationListeners"); + if( locationListeners != null ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + // Android Lint claims we need location permission for LocationManager.removeUpdates(). + // also see http://stackoverflow.com/questions/32715189/location-manager-remove-updates-permission + if( MyDebug.LOG ) + Log.d(TAG, "check for location permissions"); + boolean has_coarse_location_permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; + boolean has_fine_location_permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + if( MyDebug.LOG ) { + Log.d(TAG, "has_coarse_location_permission? " + has_coarse_location_permission); + Log.d(TAG, "has_fine_location_permission? " + has_fine_location_permission); + } + // require at least one permission to be present + if( !has_coarse_location_permission && !has_fine_location_permission ) { + if( MyDebug.LOG ) + Log.d(TAG, "location permission not available"); + return; + } + } + for(int i=0;i preloaded_bitmap_resources = new Hashtable<>(); + private ValueAnimator gallery_save_anim; + + private SoundPool sound_pool; + private SparseIntArray sound_ids; + + private TextToSpeech textToSpeech; + private boolean textToSpeechSuccess; + + private AudioListener audio_listener; + private int audio_noise_sensitivity = -1; + private SpeechRecognizer speechRecognizer; + private boolean speechRecognizerIsStarted; + + //private boolean ui_placement_right = true; + + private final ToastBoxer switch_video_toast = new ToastBoxer(); + private final ToastBoxer screen_locked_toast = new ToastBoxer(); + private final ToastBoxer changed_auto_stabilise_toast = new ToastBoxer(); + private final ToastBoxer exposure_lock_toast = new ToastBoxer(); + private final ToastBoxer audio_control_toast = new ToastBoxer(); + private boolean block_startup_toast = false; // used when returning from Settings/Popup - if we're displaying a toast anyway, don't want to display the info toast too + + // for testing; must be volatile for test project reading the state + public boolean is_test; // whether called from OpenCamera.test testing + public volatile Bitmap gallery_bitmap; + public volatile boolean test_low_memory; + public volatile boolean test_have_angle; + public volatile float test_angle; + public volatile String test_last_saved_image; + + //test lock screen///// + public PowerManager.WakeLock wl; + + @Override + protected void onCreate(Bundle savedInstanceState) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK , "My Tag"); + wl.acquire(); + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "onCreate"); + debug_time = System.currentTimeMillis(); + } + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting default preference values: " + (System.currentTimeMillis() - debug_time)); + + if( getIntent() != null && getIntent().getExtras() != null ) { + // whether called from testing + is_test = getIntent().getExtras().getBoolean("test_project"); + if( MyDebug.LOG ) + Log.d(TAG, "is_test: " + is_test); + } + if( getIntent() != null && getIntent().getExtras() != null ) { + // whether called from Take Photo widget + if( MyDebug.LOG ) + Log.d(TAG, "take_photo?: " + getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO)); + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // determine whether we should support "auto stabilise" feature + // risk of running out of memory on lower end devices, due to manipulation of large bitmaps + ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); + if( MyDebug.LOG ) { + Log.d(TAG, "standard max memory = " + activityManager.getMemoryClass() + "MB"); + Log.d(TAG, "large max memory = " + activityManager.getLargeMemoryClass() + "MB"); + } + //if( activityManager.getMemoryClass() >= 128 ) { // test + if( activityManager.getLargeMemoryClass() >= 128 ) { + supports_auto_stabilise = true; + } + if( MyDebug.LOG ) + Log.d(TAG, "supports_auto_stabilise? " + supports_auto_stabilise); + + // hack to rule out phones unlikely to have 4K video, so no point even offering the option! + // both S5 and Note 3 have 128MB standard and 512MB large heap (tested via Samsung RTL), as does Galaxy K Zoom + // also added the check for having 128MB standard heap, to support modded LG G2, which has 128MB standard, 256MB large - see https://sourceforge.net/p/opencamera/tickets/9/ + if( activityManager.getMemoryClass() >= 128 || activityManager.getLargeMemoryClass() >= 512 ) { + supports_force_video_4k = true; + } + if( MyDebug.LOG ) + Log.d(TAG, "supports_force_video_4k? " + supports_force_video_4k); + + // set up components + mainUI = new MainUI(this); + applicationInterface = new MyApplicationInterface(this, savedInstanceState); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after creating application interface: " + (System.currentTimeMillis() - debug_time)); + textFormatter = new TextFormatter(this); + + // determine whether we support Camera2 API + initCamera2Support(); + + // set up window flags for normal operation + setWindowFlagsForCamera(); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting window flags: " + (System.currentTimeMillis() - debug_time)); + + save_location_history = new SaveLocationHistory(this, "save_location_history", getStorageUtils().getSaveLocation()); + if( applicationInterface.getStorageUtils().isUsingSAF() ) { + if( MyDebug.LOG ) + Log.d(TAG, "create new SaveLocationHistory for SAF"); + save_location_history_saf = new SaveLocationHistory(this, "save_location_history_saf", getStorageUtils().getSaveLocationSAF()); + } + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after updating folder history: " + (System.currentTimeMillis() - debug_time)); + + // set up sensors + mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); + + // accelerometer sensor (for device orientation) + if( mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "found accelerometer"); + mSensorAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no support for accelerometer"); + } + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after creating accelerometer sensor: " + (System.currentTimeMillis() - debug_time)); + + // magnetic sensor (for compass direction) + if( mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "found magnetic sensor"); + mSensorMagnetic = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no support for magnetic sensor"); + } + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after creating magnetic sensor: " + (System.currentTimeMillis() - debug_time)); + + // clear any seek bars (just in case??) + mainUI.clearSeekBar(); + + // set up the camera and its preview + preview = new Preview(applicationInterface, ((ViewGroup) this.findViewById(R.id.preview))); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after creating preview: " + (System.currentTimeMillis() - debug_time)); + + // initialise on-screen button visibility + View switchCameraButton = findViewById(R.id.switch_camera); + switchCameraButton.setVisibility(preview.getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE); + View speechRecognizerButton = findViewById(R.id.audio_control); + speechRecognizerButton.setVisibility(View.GONE); // disabled by default, until the speech recognizer is created + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting button visibility: " + (System.currentTimeMillis() - debug_time)); + View pauseVideoButton = findViewById(R.id.pause_video); + pauseVideoButton.setVisibility(View.INVISIBLE); + + // listen for orientation event change + orientationEventListener = new OrientationEventListener(this) { + @Override + public void onOrientationChanged(int orientation) { + MainActivity.this.mainUI.onOrientationChanged(orientation); + } + }; + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting orientation event listener: " + (System.currentTimeMillis() - debug_time)); + + // set up gallery button long click + View galleryButton = findViewById(R.id.gallery); + galleryButton.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + //preview.showToast(null, "Long click"); + longClickedGallery(); + return true; + } + }); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting gallery long click listener: " + (System.currentTimeMillis() - debug_time)); + + // listen for gestures + gestureDetector = new GestureDetector(this, new MyGestureDetector()); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after creating gesture detector: " + (System.currentTimeMillis() - debug_time)); + + // set up listener to handle immersive mode options + View decorView = getWindow().getDecorView(); + decorView.setOnSystemUiVisibilityChangeListener + (new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + // Note that system bars will only be "visible" if none of the + // LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set. + if( !usingKitKatImmersiveMode() ) + return; + if( MyDebug.LOG ) + Log.d(TAG, "onSystemUiVisibilityChange: " + visibility); + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + if( MyDebug.LOG ) + Log.d(TAG, "system bars now visible"); + // The system bars are visible. Make any desired + // adjustments to your UI, such as showing the action bar or + // other navigational controls. + mainUI.setImmersiveMode(false); + setImmersiveTimer(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "system bars now NOT visible"); + // The system bars are NOT visible. Make any desired + // adjustments to your UI, such as hiding the action bar or + // other navigational controls. + mainUI.setImmersiveMode(true); + } + } + }); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after setting immersive mode listener: " + (System.currentTimeMillis() - debug_time)); + + // show "about" dialog for first time use; also set some per-device defaults + boolean has_done_first_time = sharedPreferences.contains(PreferenceKeys.getFirstTimePreferenceKey()); + if( MyDebug.LOG ) + Log.d(TAG, "has_done_first_time: " + has_done_first_time); + if( !has_done_first_time ) { + setDeviceDefaults(); + } + if( !has_done_first_time ) { + if( !is_test ) { + AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); + alertDialog.setTitle(R.string.app_name); + alertDialog.setMessage(R.string.intro_text); + alertDialog.setPositiveButton(R.string.intro_ok, null); + alertDialog.show(); + } + + setFirstTimeFlag(); + } + + setModeFromIntents(savedInstanceState); + + // load icons + preloadIcons(R.array.flash_icons); + preloadIcons(R.array.focus_mode_icons); + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: time after preloading icons: " + (System.currentTimeMillis() - debug_time)); + + // initialise text to speech engine + textToSpeechSuccess = false; + // run in separate thread so as to not delay startup time + new Thread(new Runnable() { + public void run() { + textToSpeech = new TextToSpeech(MainActivity.this, new TextToSpeech.OnInitListener() { + @Override + public void onInit(int status) { + if( MyDebug.LOG ) + Log.d(TAG, "TextToSpeech initialised"); + if( status == TextToSpeech.SUCCESS ) { + textToSpeechSuccess = true; + if( MyDebug.LOG ) + Log.d(TAG, "TextToSpeech succeeded"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "TextToSpeech failed"); + } + } + }); + } + }).start(); + + + if( MyDebug.LOG ) + Log.d(TAG, "onCreate: total time for Activity startup: " + (System.currentTimeMillis() - debug_time)); + } + + /* This method sets the preference defaults which are set specific for a particular device. + * This method should be called when Open Camera is run for the very first time after installation, + * or when the user has requested to "Reset settings". + */ + void setDeviceDefaults() { + if( MyDebug.LOG ) + Log.d(TAG, "setDeviceDefaults"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung"); + boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus"); + boolean is_nexus = Build.MODEL.toLowerCase(Locale.US).contains("nexus"); + boolean is_nexus6 = Build.MODEL.toLowerCase(Locale.US).contains("nexus 6"); + boolean is_pixel_phone = Build.DEVICE != null && Build.DEVICE.equals("sailfish"); + boolean is_pixel_xl_phone = Build.DEVICE != null && Build.DEVICE.equals("marlin"); + if( MyDebug.LOG ) { + Log.d(TAG, "is_samsung? " + is_samsung); + Log.d(TAG, "is_oneplus? " + is_oneplus); + Log.d(TAG, "is_nexus? " + is_nexus); + Log.d(TAG, "is_nexus6? " + is_nexus6); + Log.d(TAG, "is_pixel_phone? " + is_pixel_phone); + Log.d(TAG, "is_pixel_xl_phone? " + is_pixel_xl_phone); + } + if( is_samsung || is_oneplus ) { + // workaround needed for Samsung S7 at least (tested on Samsung RTL) + // workaround needed for OnePlus 3 at least (see http://forum.xda-developers.com/oneplus-3/help/camera2-support-t3453103 ) + if( MyDebug.LOG ) + Log.d(TAG, "set fake flash for camera2"); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getCamera2FakeFlashPreferenceKey(), true); + editor.apply(); + } + if( is_nexus6 ) { + // Nexus 6 captureBurst() started having problems with Android 7 upgrade - images appeared in wrong order (and with wrong order of shutter speeds in exif info), as well as problems with the camera failing with serious errors + // we set this even for Nexus 6 devices not on Android 7, as at some point they'll likely be upgraded to Android 7 + if( MyDebug.LOG ) + Log.d(TAG, "disable fast burst for camera2"); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getCamera2FastBurstPreferenceKey(), false); + editor.apply(); + } + if( is_nexus || is_pixel_phone || is_pixel_xl_phone ) { + // generally continuous picture mode works better than auto-focus, though we want to make sure it works okay on the device + // Nexus devices seem to work fine with continuous mode, and we know that Google's camera uses continuous focus + if( MyDebug.LOG ) + Log.d(TAG, "set continuous focus mode for photo"); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getFocusPreferenceKey(0, false), "focus_mode_continuous_picture"); + editor.apply(); + } + } + + /** Switches modes if required, if called from a relevant intent/tile. + */ + private void setModeFromIntents(Bundle savedInstanceState) { + if( MyDebug.LOG ) + Log.d(TAG, "setModeFromIntents"); + if( savedInstanceState != null ) { + // If we're restoring from a saved state, we shouldn't be resetting any modes + if( MyDebug.LOG ) + Log.d(TAG, "restoring from saved state"); + return; + } + String action = this.getIntent().getAction(); + if( MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(action) || MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "launching from video intent"); + applicationInterface.setVideoPref(true); + } + else if( MediaStore.ACTION_IMAGE_CAPTURE.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "launching from photo intent"); + applicationInterface.setVideoPref(false); + } + else if( MyTileService.TILE_ID.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "launching from quick settings tile for Open Camera: photo mode"); + applicationInterface.setVideoPref(false); + } + else if( MyTileServiceVideo.TILE_ID.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "launching from quick settings tile for Open Camera: video mode"); + applicationInterface.setVideoPref(true); + } + else if( MyTileServiceFrontCamera.TILE_ID.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "launching from quick settings tile for Open Camera: selfie mode"); + for(int i=0;i= Build.VERSION_CODES.LOLLIPOP ) { + CameraControllerManager2 manager2 = new CameraControllerManager2(this); + supports_camera2 = true; + if( manager2.getNumberOfCameras() == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "Camera2 reports 0 cameras"); + supports_camera2 = false; + } + for(int i=0;i= Build.VERSION_CODES.M ) { + // see note in HDRProcessor.onDestroy() - but from Android M, renderscript contexts are released with releaseAllContexts() + // doc for releaseAllContexts() says "If no contexts have been created this function does nothing" + RenderScript.releaseAllContexts(); + } + // Need to recycle to avoid out of memory when running tests - probably good practice to do anyway + for(Map.Entry entry : preloaded_bitmap_resources.entrySet()) { + if( MyDebug.LOG ) + Log.d(TAG, "recycle: " + entry.getKey()); + entry.getValue().recycle(); + } + preloaded_bitmap_resources.clear(); + if( textToSpeech != null ) { + // http://stackoverflow.com/questions/4242401/tts-error-leaked-serviceconnection-android-speech-tts-texttospeech-solved + Log.d(TAG, "free textToSpeech"); + textToSpeech.stop(); + textToSpeech.shutdown(); + textToSpeech = null; + } + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + private void setFirstTimeFlag() { + if( MyDebug.LOG ) + Log.d(TAG, "setFirstTimeFlag"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getFirstTimePreferenceKey(), true); + editor.apply(); + } + + // for audio "noise" trigger option + private int last_level = -1; + private long time_quiet_loud = -1; + private long time_last_audio_trigger_photo = -1; + + /** Listens to audio noise and decides when there's been a "loud" noise to trigger taking a photo. + */ + public void onAudio(int level) { + boolean audio_trigger = false; + /*if( level > 150 ) { + if( MyDebug.LOG ) + Log.d(TAG, "loud noise!: " + level); + audio_trigger = true; + }*/ + + if( last_level == -1 ) { + last_level = level; + return; + } + int diff = level - last_level; + + if( MyDebug.LOG ) + Log.d(TAG, "noise_sensitivity: " + audio_noise_sensitivity); + + if( diff > audio_noise_sensitivity ) { + if( MyDebug.LOG ) + Log.d(TAG, "got louder!: " + last_level + " to " + level + " , diff: " + diff); + time_quiet_loud = System.currentTimeMillis(); + if( MyDebug.LOG ) + Log.d(TAG, " time: " + time_quiet_loud); + } + else if( diff < -audio_noise_sensitivity && time_quiet_loud != -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "got quieter!: " + last_level + " to " + level + " , diff: " + diff); + long time_now = System.currentTimeMillis(); + long duration = time_now - time_quiet_loud; + if( MyDebug.LOG ) { + Log.d(TAG, "stopped being loud - was loud since :" + time_quiet_loud); + Log.d(TAG, " time_now: " + time_now); + Log.d(TAG, " duration: " + duration); + } + if( duration < 1500 ) { + if( MyDebug.LOG ) + Log.d(TAG, "audio_trigger set"); + audio_trigger = true; + } + time_quiet_loud = -1; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "audio level: " + last_level + " to " + level + " , diff: " + diff); + } + + last_level = level; + + if( audio_trigger ) { + if( MyDebug.LOG ) + Log.d(TAG, "audio trigger"); + // need to run on UI thread so that this function returns quickly (otherwise we'll have lag in processing the audio) + // but also need to check we're not currently taking a photo or on timer, so we don't repeatedly queue up takePicture() calls, or cancel a timer + long time_now = System.currentTimeMillis(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean want_audio_listener = sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("noise"); + if( time_last_audio_trigger_photo != -1 && time_now - time_last_audio_trigger_photo < 5000 ) { + // avoid risk of repeatedly being triggered - as well as problem of being triggered again by the camera's own "beep"! + if( MyDebug.LOG ) + Log.d(TAG, "ignore loud noise due to too soon since last audio triggerred photo:" + (time_now - time_last_audio_trigger_photo)); + } + else if( !want_audio_listener ) { + // just in case this is a callback from an AudioListener before it's been freed (e.g., if there's a loud noise when exiting settings after turning the option off + if( MyDebug.LOG ) + Log.d(TAG, "ignore loud noise due to audio listener option turned off"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "audio trigger from loud noise"); + time_last_audio_trigger_photo = time_now; + audioTrigger(); + } + } + } + + /* Audio trigger - either loud sound, or speech recognition. + * This performs some additional checks before taking a photo. + */ + private void audioTrigger() { + if( MyDebug.LOG ) + Log.d(TAG, "ignore audio trigger due to popup open"); + if( popupIsOpen() ) { + if( MyDebug.LOG ) + Log.d(TAG, "ignore audio trigger due to popup open"); + } + else if( camera_in_background ) { + if( MyDebug.LOG ) + Log.d(TAG, "ignore audio trigger due to camera in background"); + } + else if( preview.isTakingPhotoOrOnTimer() ) { + if( MyDebug.LOG ) + Log.d(TAG, "ignore audio trigger due to already taking photo or on timer"); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "schedule take picture due to loud noise"); + //takePicture(); + this.runOnUiThread(new Runnable() { + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "taking picture due to audio trigger"); + takePicture(); + } + }); + } + } + + @SuppressWarnings("deprecation") + public boolean onKeyDown(int keyCode, KeyEvent event) { + if( MyDebug.LOG ) + Log.d(TAG, "onKeyDown: " + keyCode); + boolean handled = mainUI.onKeyDown(keyCode, event); + if( handled ) + return true; + return super.onKeyDown(keyCode, event); + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + if( MyDebug.LOG ) + Log.d(TAG, "onKeyUp: " + keyCode); + mainUI.onKeyUp(keyCode, event); + return super.onKeyUp(keyCode, event); + } + + public void zoomIn() { + mainUI.changeSeekbar(R.id.zoom_seekbar, -1); + } + + public void zoomOut() { + mainUI.changeSeekbar(R.id.zoom_seekbar, 1); + } + + public void changeExposure(int change) { + mainUI.changeSeekbar(R.id.exposure_seekbar, change); + } + + public void changeISO(int change) { + mainUI.changeSeekbar(R.id.iso_seekbar, change); + } + + public void changeFocusDistance(int change) { + mainUI.changeSeekbar(R.id.focus_seekbar, change); + } + + private final SensorEventListener accelerometerListener = new SensorEventListener() { + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + preview.onAccelerometerSensorChanged(event); + } + }; + + private final SensorEventListener magneticListener = new SensorEventListener() { + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + preview.onMagneticSensorChanged(event); + } + }; + + @Override + protected void onResume() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "onResume"); + debug_time = System.currentTimeMillis(); + } + super.onResume(); + + // Set black window background; also needed if we hide the virtual buttons in immersive mode + // Note that we do it here rather than customising the theme's android:windowBackground, so this doesn't affect other views - in particular, the MyPreferenceFragment settings + getWindow().getDecorView().getRootView().setBackgroundColor(Color.BLACK); + + mSensorManager.registerListener(accelerometerListener, mSensorAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); + mSensorManager.registerListener(magneticListener, mSensorMagnetic, SensorManager.SENSOR_DELAY_NORMAL); + orientationEventListener.enable(); + + initSpeechRecognizer(); + initLocation(); + initSound(); + loadSound(R.raw.beep); + loadSound(R.raw.beep_hi); + + mainUI.layoutUI(); + + updateGalleryIcon(); // update in case images deleted whilst idle + + preview.onResume(); + + if( MyDebug.LOG ) { + Log.d(TAG, "onResume: total time to resume: " + (System.currentTimeMillis() - debug_time)); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if( MyDebug.LOG ) + Log.d(TAG, "onWindowFocusChanged: " + hasFocus); + super.onWindowFocusChanged(hasFocus); + if( !this.camera_in_background && hasFocus ) { + // low profile mode is cleared when app goes into background + // and for Kit Kat immersive mode, we want to set up the timer + // we do in onWindowFocusChanged rather than onResume(), to also catch when window lost focus due to notification bar being dragged down (which prevents resetting of immersive mode) + initImmersiveMode(); + } + } + + @Override + protected void onPause() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "onPause"); + debug_time = System.currentTimeMillis(); + } + waitUntilImageQueueEmpty(); // so we don't risk losing any images + super.onPause(); // docs say to call this before freeing other things + mainUI.destroyPopup(); + mSensorManager.unregisterListener(accelerometerListener); + mSensorManager.unregisterListener(magneticListener); + orientationEventListener.disable(); + freeAudioListener(false); + freeSpeechRecognizer(); + applicationInterface.getLocationSupplier().freeLocationListeners(); + releaseSound(); + applicationInterface.clearLastImages(); // this should happen when pausing the preview, but call explicitly just to be safe + preview.onPause(); + if( MyDebug.LOG ) { + Log.d(TAG, "onPause: total time to pause: " + (System.currentTimeMillis() - debug_time)); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if( MyDebug.LOG ) + Log.d(TAG, "onConfigurationChanged()"); + // configuration change can include screen orientation (landscape/portrait) when not locked (when settings is open) + // needed if app is paused/resumed when settings is open and device is in portrait mode + preview.setCameraDisplayOrientation(); + super.onConfigurationChanged(newConfig); + } + + public void waitUntilImageQueueEmpty() { + if( MyDebug.LOG ) + Log.d(TAG, "waitUntilImageQueueEmpty"); + applicationInterface.getImageSaver().waitUntilDone(); + } + + public void clickedTakePhoto(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedTakePhoto"); + this.takePicture(); + } + + public void clickedPauseVideo(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedPauseVideo"); + if( preview.isVideoRecording() ) { // just in case + preview.pauseVideo(); + mainUI.setPauseVideoContentDescription(); + } + } + + public void clickedAudioControl(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedAudioControl"); + // check hasAudioControl just in case! + if( !hasAudioControl() ) { + if( MyDebug.LOG ) + Log.e(TAG, "clickedAudioControl, but hasAudioControl returns false!"); + return; + } + this.closePopup(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String audio_control = sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none"); + if( audio_control.equals("voice") && speechRecognizer != null ) { + if( speechRecognizerIsStarted ) { + speechRecognizer.stopListening(); + speechRecognizerStopped(); + } + else { + preview.showToast(audio_control_toast, R.string.speech_recognizer_started); + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en_US"); // since we listen for "cheese", ensure this works even for devices with different language settings + speechRecognizer.startListening(intent); + speechRecognizerStarted(); + } + } + else if( audio_control.equals("noise") ){ + if( audio_listener != null ) { + freeAudioListener(false); + } + else { + preview.showToast(audio_control_toast, R.string.audio_listener_started); + startAudioListener(); + } + } + } + + private void speechRecognizerStarted() { + if( MyDebug.LOG ) + Log.d(TAG, "speechRecognizerStarted"); + mainUI.audioControlStarted(); + speechRecognizerIsStarted = true; + } + + private void speechRecognizerStopped() { + if( MyDebug.LOG ) + Log.d(TAG, "speechRecognizerStopped"); + mainUI.audioControlStopped(); + speechRecognizerIsStarted = false; + } + + /* Returns the cameraId that the "Switch camera" button will switch to. + */ + public int getNextCameraId() { + if( MyDebug.LOG ) + Log.d(TAG, "getNextCameraId"); + int cameraId = preview.getCameraId(); + if( MyDebug.LOG ) + Log.d(TAG, "current cameraId: " + cameraId); + if( this.preview.canSwitchCamera() ) { + int n_cameras = preview.getCameraControllerManager().getNumberOfCameras(); + cameraId = (cameraId+1) % n_cameras; + } + if( MyDebug.LOG ) + Log.d(TAG, "next cameraId: " + cameraId); + return cameraId; + } + + public void clickedSwitchCamera(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedSwitchCamera"); + this.closePopup(); + if( this.preview.canSwitchCamera() ) { + int cameraId = getNextCameraId(); + View switchCameraButton = findViewById(R.id.switch_camera); + switchCameraButton.setEnabled(false); // prevent slowdown if user repeatedly clicks + this.preview.setCamera(cameraId); + switchCameraButton.setEnabled(true); + mainUI.setSwitchCameraContentDescription(); + } + } + + public void clickedSwitchVideo(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedSwitchVideo"); + this.closePopup(); + View switchVideoButton = findViewById(R.id.switch_video); + switchVideoButton.setEnabled(false); // prevent slowdown if user repeatedly clicks + this.preview.switchVideo(false); + switchVideoButton.setEnabled(true); + + mainUI.setTakePhotoIcon(); + if( !block_startup_toast ) { + this.showPhotoVideoToast(true); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void clickedExposure(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedExposure"); + mainUI.toggleExposureUI(); + } + + private static double seekbarScaling(double frac) { + // For various seekbars, we want to use a non-linear scaling, so user has more control over smaller values + return (Math.pow(100.0, frac) - 1.0) / 99.0; + } + + private static double seekbarScalingInverse(double scaling) { + return Math.log(99.0*scaling + 1.0) / Math.log(100.0); + } + + private void setProgressSeekbarScaled(SeekBar seekBar, double min_value, double max_value, double value) { + seekBar.setMax(100); + double scaling = (value - min_value)/(max_value - min_value); + double frac = MainActivity.seekbarScalingInverse(scaling); + int percent = (int)(frac*100.0 + 0.5); // add 0.5 for rounding + if( percent < 0 ) + percent = 0; + else if( percent > 100 ) + percent = 100; + seekBar.setProgress(percent); + } + + public void clickedExposureLock(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedExposureLock"); + this.preview.toggleExposureLock(); + ImageButton exposureLockButton = (ImageButton) findViewById(R.id.exposure_lock); + exposureLockButton.setImageResource(preview.isExposureLocked() ? R.drawable.exposure_locked : R.drawable.exposure_unlocked); + preview.showToast(exposure_lock_toast, preview.isExposureLocked() ? R.string.exposure_locked : R.string.exposure_unlocked); + } + + public void clickedSettings(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedSettings"); + openSettings(); + } + + public boolean popupIsOpen() { + return mainUI.popupIsOpen(); + } + + // for testing + public View getPopupButton(String key) { + return mainUI.getPopupButton(key); + } + + public void closePopup() { + mainUI.closePopup(); + } + + public Bitmap getPreloadedBitmap(int resource) { + return this.preloaded_bitmap_resources.get(resource); + } + + public void clickedPopupSettings(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedPopupSettings"); + mainUI.togglePopupSettings(); + } + + public void openSettings() { + if( MyDebug.LOG ) + Log.d(TAG, "openSettings"); + waitUntilImageQueueEmpty(); // in theory not needed as we could continue running in the background, but best to be safe + closePopup(); + preview.cancelTimer(); // best to cancel any timer, in case we take a photo while settings window is open, or when changing settings + preview.stopVideo(false); // important to stop video, as we'll be changing camera parameters when the settings window closes + stopAudioListeners(); + + Bundle bundle = new Bundle(); + bundle.putInt("cameraId", this.preview.getCameraId()); + bundle.putString("camera_api", this.preview.getCameraAPI()); + bundle.putBoolean("using_android_l", this.preview.usingCamera2API()); + bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise); + bundle.putBoolean("supports_force_video_4k", this.supports_force_video_4k); + bundle.putBoolean("supports_camera2", this.supports_camera2); + bundle.putBoolean("supports_face_detection", this.preview.supportsFaceDetection()); + bundle.putBoolean("supports_raw", this.preview.supportsRaw()); + bundle.putBoolean("supports_hdr", this.supportsHDR()); + bundle.putBoolean("supports_expo_bracketing", this.supportsExpoBracketing()); + bundle.putBoolean("supports_video_stabilization", this.preview.supportsVideoStabilization()); + bundle.putBoolean("can_disable_shutter_sound", this.preview.canDisableShutterSound()); + + putBundleExtra(bundle, "color_effects", this.preview.getSupportedColorEffects()); + putBundleExtra(bundle, "scene_modes", this.preview.getSupportedSceneModes()); + putBundleExtra(bundle, "white_balances", this.preview.getSupportedWhiteBalances()); + putBundleExtra(bundle, "isos", this.preview.getSupportedISOs()); + bundle.putString("iso_key", this.preview.getISOKey()); + if( this.preview.getCameraController() != null ) { + bundle.putString("parameters_string", preview.getCameraController().getParametersString()); + } + + List preview_sizes = this.preview.getSupportedPreviewSizes(); + if( preview_sizes != null ) { + int [] widths = new int[preview_sizes.size()]; + int [] heights = new int[preview_sizes.size()]; + int i=0; + for(CameraController.Size size: preview_sizes) { + widths[i] = size.width; + heights[i] = size.height; + i++; + } + bundle.putIntArray("preview_widths", widths); + bundle.putIntArray("preview_heights", heights); + } + bundle.putInt("preview_width", preview.getCurrentPreviewSize().width); + bundle.putInt("preview_height", preview.getCurrentPreviewSize().height); + + List sizes = this.preview.getSupportedPictureSizes(); + if( sizes != null ) { + int [] widths = new int[sizes.size()]; + int [] heights = new int[sizes.size()]; + int i=0; + for(CameraController.Size size: sizes) { + widths[i] = size.width; + heights[i] = size.height; + i++; + } + bundle.putIntArray("resolution_widths", widths); + bundle.putIntArray("resolution_heights", heights); + } + if( preview.getCurrentPictureSize() != null ) { + bundle.putInt("resolution_width", preview.getCurrentPictureSize().width); + bundle.putInt("resolution_height", preview.getCurrentPictureSize().height); + } + + List video_quality = this.preview.getVideoQualityHander().getSupportedVideoQuality(); + if( video_quality != null && this.preview.getCameraController() != null ) { + String [] video_quality_arr = new String[video_quality.size()]; + String [] video_quality_string_arr = new String[video_quality.size()]; + int i=0; + for(String value: video_quality) { + video_quality_arr[i] = value; + video_quality_string_arr[i] = this.preview.getCamcorderProfileDescription(value); + i++; + } + bundle.putStringArray("video_quality", video_quality_arr); + bundle.putStringArray("video_quality_string", video_quality_string_arr); + } + if( preview.getVideoQualityHander().getCurrentVideoQuality() != null ) { + bundle.putString("current_video_quality", preview.getVideoQualityHander().getCurrentVideoQuality()); + } + CamcorderProfile camcorder_profile = preview.getCamcorderProfile(); + bundle.putInt("video_frame_width", camcorder_profile.videoFrameWidth); + bundle.putInt("video_frame_height", camcorder_profile.videoFrameHeight); + bundle.putInt("video_bit_rate", camcorder_profile.videoBitRate); + bundle.putInt("video_frame_rate", camcorder_profile.videoFrameRate); + + List video_sizes = this.preview.getVideoQualityHander().getSupportedVideoSizes(); + if( video_sizes != null ) { + int [] widths = new int[video_sizes.size()]; + int [] heights = new int[video_sizes.size()]; + int i=0; + for(CameraController.Size size: video_sizes) { + widths[i] = size.width; + heights[i] = size.height; + i++; + } + bundle.putIntArray("video_widths", widths); + bundle.putIntArray("video_heights", heights); + } + + putBundleExtra(bundle, "flash_values", this.preview.getSupportedFlashValues()); + putBundleExtra(bundle, "focus_values", this.preview.getSupportedFocusValues()); + + setWindowFlagsForSettings(); + MyPreferenceFragment fragment = new MyPreferenceFragment(); + fragment.setArguments(bundle); + // use commitAllowingStateLoss() instead of commit(), does to "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState" crash seen on Google Play + // see http://stackoverflow.com/questions/7575921/illegalstateexception-can-not-perform-this-action-after-onsaveinstancestate-wit + getFragmentManager().beginTransaction().add(R.id.prefs_container, fragment, "PREFERENCE_FRAGMENT").addToBackStack(null).commitAllowingStateLoss(); + } + + public void updateForSettings() { + updateForSettings(null); + } + + public void updateForSettings(String toast_message) { + if( MyDebug.LOG ) { + Log.d(TAG, "updateForSettings()"); + if( toast_message != null ) { + Log.d(TAG, "toast_message: " + toast_message); + } + } + // make sure we're into continuous video mode + // workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted + // so to be safe, we always reset to continuous video mode, and then reset it afterwards + String saved_focus_value = preview.updateFocusForVideo(); // n.b., may be null if focus mode not changed + if( MyDebug.LOG ) + Log.d(TAG, "saved_focus_value: " + saved_focus_value); + + if( MyDebug.LOG ) + Log.d(TAG, "update folder history"); + save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true); + // no need to update save_location_history_saf, as we always do this in onActivityResult() + + // update camera for changes made in prefs - do this without closing and reopening the camera app if possible for speed! + // but need workaround for Nexus 7 bug, where scene mode doesn't take effect unless the camera is restarted - I can reproduce this with other 3rd party camera apps, so may be a Nexus 7 issue... + boolean need_reopen = false; + if( preview.getCameraController() != null ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String scene_mode = preview.getCameraController().getSceneMode(); + if( MyDebug.LOG ) + Log.d(TAG, "scene mode was: " + scene_mode); + String key = PreferenceKeys.getSceneModePreferenceKey(); + String value = sharedPreferences.getString(key, preview.getCameraController().getDefaultSceneMode()); + if( !value.equals(scene_mode) ) { + if( MyDebug.LOG ) + Log.d(TAG, "scene mode changed to: " + value); + need_reopen = true; + } + else { + if( applicationInterface.useCamera2() ) { + // need to reopen if fake flash mode changed, as it changes the available camera features, and we can only set this after opening the camera + boolean camera2_fake_flash = preview.getCameraController().getUseCamera2FakeFlash(); + if( MyDebug.LOG ) + Log.d(TAG, "camera2_fake_flash was: " + camera2_fake_flash); + if( applicationInterface.useCamera2FakeFlash() != camera2_fake_flash ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera2_fake_flash changed"); + need_reopen = true; + } + } + } + } + + mainUI.layoutUI(); // needed in case we've changed left/right handed UI + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + if( sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("none") ) { + // ensure icon is invisible if switching from audio control enabled to disabled + // (if enabling it, we'll make the icon visible later on) + View speechRecognizerButton = findViewById(R.id.audio_control); + speechRecognizerButton.setVisibility(View.GONE); + } + initSpeechRecognizer(); // in case we've enabled or disabled speech recognizer + initLocation(); // in case we've enabled or disabled GPS + if( toast_message != null ) + block_startup_toast = true; + if( need_reopen || preview.getCameraController() == null ) { // if camera couldn't be opened before, might as well try again + preview.onPause(); + preview.onResume(); + } + else { + preview.setCameraDisplayOrientation(); // need to call in case the preview rotation option was changed + preview.pausePreview(); + preview.setupCamera(false); + } + block_startup_toast = false; + if( toast_message != null && toast_message.length() > 0 ) + preview.showToast(null, toast_message); + + if( saved_focus_value != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "switch focus back to: " + saved_focus_value); + preview.updateFocus(saved_focus_value, true, false); + } + } + + private MyPreferenceFragment getPreferenceFragment() { + return (MyPreferenceFragment)getFragmentManager().findFragmentByTag("PREFERENCE_FRAGMENT"); + } + + @Override + public void onBackPressed() { + final MyPreferenceFragment fragment = getPreferenceFragment(); + if( screen_is_locked ) { + preview.showToast(screen_locked_toast, R.string.screen_is_locked); + return; + } + if( fragment != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "close settings"); + setWindowFlagsForCamera(); + updateForSettings(); + } + else { + if( popupIsOpen() ) { + closePopup(); + return; + } + } + super.onBackPressed(); + } + + public boolean usingKitKatImmersiveMode() { + // whether we are using a Kit Kat style immersive mode (either hiding GUI, or everything) + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String immersive_mode = sharedPreferences.getString(PreferenceKeys.getImmersiveModePreferenceKey(), "immersive_mode_low_profile"); + if( immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything") ) + return true; + } + return false; + } + public boolean usingKitKatImmersiveModeEverything() { + // whether we are using a Kit Kat style immersive mode for everything + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String immersive_mode = sharedPreferences.getString(PreferenceKeys.getImmersiveModePreferenceKey(), "immersive_mode_low_profile"); + if( immersive_mode.equals("immersive_mode_everything") ) + return true; + } + return false; + } + + + private Handler immersive_timer_handler = null; + private Runnable immersive_timer_runnable = null; + + private void setImmersiveTimer() { + if( immersive_timer_handler != null && immersive_timer_runnable != null ) { + immersive_timer_handler.removeCallbacks(immersive_timer_runnable); + } + immersive_timer_handler = new Handler(); + immersive_timer_handler.postDelayed(immersive_timer_runnable = new Runnable(){ + @Override + public void run(){ + if( MyDebug.LOG ) + Log.d(TAG, "setImmersiveTimer: run"); + if( !camera_in_background && !popupIsOpen() && usingKitKatImmersiveMode() ) + setImmersiveMode(true); + } + }, 5000); + } + + public void initImmersiveMode() { + if( !usingKitKatImmersiveMode() ) { + setImmersiveMode(true); + } + else { + // don't start in immersive mode, only after a timer + setImmersiveTimer(); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + void setImmersiveMode(boolean on) { + if( MyDebug.LOG ) + Log.d(TAG, "setImmersiveMode: " + on); + // n.b., preview.setImmersiveMode() is called from onSystemUiVisibilityChange() + if( on ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && usingKitKatImmersiveMode() ) { + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + else { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String immersive_mode = sharedPreferences.getString(PreferenceKeys.getImmersiveModePreferenceKey(), "immersive_mode_low_profile"); + if( immersive_mode.equals("immersive_mode_low_profile") ) + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + else + getWindow().getDecorView().setSystemUiVisibility(0); + } + } + else + getWindow().getDecorView().setSystemUiVisibility(0); + } + + /** Sets the brightness level for normal operation (when camera preview is visible). + * If force_max is true, this always forces maximum brightness; otherwise this depends on user preference. + */ + void setBrightnessForCamera(boolean force_max) { + if( MyDebug.LOG ) + Log.d(TAG, "setBrightnessForCamera"); + // set screen to max brightness - see http://stackoverflow.com/questions/11978042/android-screen-brightness-max-value + // done here rather than onCreate, so that changing it in preferences takes effect without restarting app + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + WindowManager.LayoutParams layout = getWindow().getAttributes(); + if( force_max || sharedPreferences.getBoolean(PreferenceKeys.getMaxBrightnessPreferenceKey(), true) ) { + layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL; + } + else { + layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; + } + getWindow().setAttributes(layout); + } + + /** Sets the window flags for normal operation (when camera preview is visible). + */ + public void setWindowFlagsForCamera() { + if( MyDebug.LOG ) + Log.d(TAG, "setWindowFlagsForCamera"); + /*{ + Intent intent = new Intent(this, MyWidgetProvider.class); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + AppWidgetManager widgetManager = AppWidgetManager.getInstance(this); + ComponentName widgetComponent = new ComponentName(this, MyWidgetProvider.class); + int[] widgetIds = widgetManager.getAppWidgetIds(widgetComponent); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds); + sendBroadcast(intent); + }*/ + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // force to landscape mode + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + //setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); // testing for devices with unusual sensor orientation (e.g., Nexus 5X) + // keep screen active - see http://stackoverflow.com/questions/2131948/force-screen-on + if( sharedPreferences.getBoolean(PreferenceKeys.getKeepDisplayOnPreferenceKey(), true) ) { + if( MyDebug.LOG ) + Log.d(TAG, "do keep screen on"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "don't keep screen on"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + if( sharedPreferences.getBoolean(PreferenceKeys.getShowWhenLockedPreferenceKey(), true) ) { + if( MyDebug.LOG ) + Log.d(TAG, "do show when locked"); + // keep Open Camera on top of screen-lock (will still need to unlock when going to gallery or settings) + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "don't show when locked"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + + setBrightnessForCamera(false); + + initImmersiveMode(); + camera_in_background = false; + } + + /** Sets the window flags for when the settings window is open. + */ + public void setWindowFlagsForSettings() { + // allow screen rotation + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + // revert to standard screen blank behaviour + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + // settings should still be protected by screen lock + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + { + WindowManager.LayoutParams layout = getWindow().getAttributes(); + layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; + getWindow().setAttributes(layout); + } + + setImmersiveMode(false); + camera_in_background = true; + } + + public void showPreview(boolean show) { + if( MyDebug.LOG ) + Log.d(TAG, "showPreview: " + show); + final ViewGroup container = (ViewGroup)findViewById(R.id.hide_container); + container.setBackgroundColor(Color.BLACK); + container.setAlpha(show ? 0.0f : 1.0f); + } + + /** Shows the default "blank" gallery icon, when we don't have a thumbnail available. + */ + private void updateGalleryIconToBlank() { + if( MyDebug.LOG ) + Log.d(TAG, "updateGalleryIconToBlank"); + ImageButton galleryButton = (ImageButton) this.findViewById(R.id.gallery); + int bottom = galleryButton.getPaddingBottom(); + int top = galleryButton.getPaddingTop(); + int right = galleryButton.getPaddingRight(); + int left = galleryButton.getPaddingLeft(); + /*if( MyDebug.LOG ) + Log.d(TAG, "padding: " + bottom);*/ + galleryButton.setImageBitmap(null); + galleryButton.setImageResource(R.drawable.gallery); + // workaround for setImageResource also resetting padding, Android bug + galleryButton.setPadding(left, top, right, bottom); + gallery_bitmap = null; + } + + /** Shows a thumbnail for the gallery icon. + */ + void updateGalleryIcon(Bitmap thumbnail) { + if( MyDebug.LOG ) + Log.d(TAG, "updateGalleryIcon: " + thumbnail); + ImageButton galleryButton = (ImageButton) this.findViewById(R.id.gallery); + galleryButton.setImageBitmap(thumbnail); + gallery_bitmap = thumbnail; + } + + /** Updates the gallery icon by searching for the most recent photo. + * Launches the task in a separate thread. + */ + public void updateGalleryIcon() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "updateGalleryIcon"); + debug_time = System.currentTimeMillis(); + } + + new AsyncTask() { + private static final String TAG = "MainActivity/AsyncTask"; + + /** The system calls this to perform work in a worker thread and + * delivers it the parameters given to AsyncTask.execute() */ + protected Bitmap doInBackground(Void... params) { + if( MyDebug.LOG ) + Log.d(TAG, "doInBackground"); + StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); + Bitmap thumbnail = null; + KeyguardManager keyguard_manager = (KeyguardManager)MainActivity.this.getSystemService(Context.KEYGUARD_SERVICE); + boolean is_locked = keyguard_manager != null && keyguard_manager.inKeyguardRestrictedInputMode(); + if( MyDebug.LOG ) + Log.d(TAG, "is_locked?: " + is_locked); + if( media != null && getContentResolver() != null && !is_locked ) { + // check for getContentResolver() != null, as have had reported Google Play crashes + try { + if( media.video ) { + thumbnail = MediaStore.Video.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Video.Thumbnails.MINI_KIND, null); + } + else { + thumbnail = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Images.Thumbnails.MINI_KIND, null); + } + } + catch(NoClassDefFoundError exception) { + // have had Google Play crashes from new ExifInterface() for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn) + if( MyDebug.LOG ) + Log.e(TAG, "exif orientation NoClassDefFoundError"); + exception.printStackTrace(); + } + if( thumbnail != null ) { + if( media.orientation != 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "thumbnail size is " + thumbnail.getWidth() + " x " + thumbnail.getHeight()); + Matrix matrix = new Matrix(); + matrix.setRotate(media.orientation, thumbnail.getWidth() * 0.5f, thumbnail.getHeight() * 0.5f); + try { + Bitmap rotated_thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.getWidth(), thumbnail.getHeight(), matrix, true); + // careful, as rotated_thumbnail is sometimes not a copy! + if( rotated_thumbnail != thumbnail ) { + thumbnail.recycle(); + thumbnail = rotated_thumbnail; + } + } + catch(Throwable t) { + if( MyDebug.LOG ) + Log.d(TAG, "failed to rotate thumbnail"); + } + } + } + } + return thumbnail; + } + + /** The system calls this to perform work in the UI thread and delivers + * the result from doInBackground() */ + protected void onPostExecute(Bitmap thumbnail) { + if( MyDebug.LOG ) + Log.d(TAG, "onPostExecute"); + // since we're now setting the thumbnail to the latest media on disk, we need to make sure clicking the Gallery goes to this + applicationInterface.getStorageUtils().clearLastMediaScanned(); + if( thumbnail != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "set gallery button to thumbnail"); + updateGalleryIcon(thumbnail); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "set gallery button to blank"); + updateGalleryIconToBlank(); + } + } + }.execute(); + + if( MyDebug.LOG ) + Log.d(TAG, "updateGalleryIcon: total time to update gallery icon: " + (System.currentTimeMillis() - debug_time)); + } + + void savingImage(final boolean started) { + if( MyDebug.LOG ) + Log.d(TAG, "savingImage: " + started); + + this.runOnUiThread(new Runnable() { + public void run() { + final ImageButton galleryButton = (ImageButton) findViewById(R.id.gallery); + if( started ) { + //galleryButton.setColorFilter(0x80ffffff, PorterDuff.Mode.MULTIPLY); + if( gallery_save_anim == null ) { + gallery_save_anim = ValueAnimator.ofInt(Color.argb(200, 255, 255, 255), Color.argb(63, 255, 255, 255)); + gallery_save_anim.setEvaluator(new ArgbEvaluator()); + gallery_save_anim.setRepeatCount(ValueAnimator.INFINITE); + gallery_save_anim.setRepeatMode(ValueAnimator.REVERSE); + gallery_save_anim.setDuration(500); + } + gallery_save_anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + galleryButton.setColorFilter((Integer)animation.getAnimatedValue(), PorterDuff.Mode.MULTIPLY); + } + }); + gallery_save_anim.start(); + } + else + if( gallery_save_anim != null ) { + gallery_save_anim.cancel(); + } + galleryButton.setColorFilter(null); + } + }); + } + + public void clickedGallery(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedGallery"); + //Intent intent = new Intent(Intent.ACTION_VIEW, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + Uri uri = applicationInterface.getStorageUtils().getLastMediaScanned(); + if( uri == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "go to latest media"); + StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); + if( media != null ) { + uri = media.uri; + } + } + + if( uri != null ) { + // check uri exists + if( MyDebug.LOG ) + Log.d(TAG, "found most recent uri: " + uri); + try { + ContentResolver cr = getContentResolver(); + ParcelFileDescriptor pfd = cr.openFileDescriptor(uri, "r"); + if( pfd == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "uri no longer exists (1): " + uri); + uri = null; + } + else { + pfd.close(); + } + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.d(TAG, "uri no longer exists (2): " + uri); + uri = null; + } + } + if( uri == null ) { + uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + if( !is_test ) { + // don't do if testing, as unclear how to exit activity to finish test (for testGallery()) + if( MyDebug.LOG ) + Log.d(TAG, "launch uri:" + uri); + final String REVIEW_ACTION = "com.android.camera.action.REVIEW"; + try { + // REVIEW_ACTION means we can view video files without autoplaying + Intent intent = new Intent(REVIEW_ACTION, uri); + this.startActivity(intent); + } + catch(ActivityNotFoundException e) { + if( MyDebug.LOG ) + Log.d(TAG, "REVIEW_ACTION intent didn't work, try ACTION_VIEW"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + // from http://stackoverflow.com/questions/11073832/no-activity-found-to-handle-intent - needed to fix crash if no gallery app installed + //Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("blah")); // test + if( intent.resolveActivity(getPackageManager()) != null ) { + this.startActivity(intent); + } + else{ + preview.showToast(null, R.string.no_gallery_app); + } + } + } + } + + /** Opens the Storage Access Framework dialog to select a folder. + * @param from_preferences Whether called from the Preferences + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + void openFolderChooserDialogSAF(boolean from_preferences) { + if( MyDebug.LOG ) + Log.d(TAG, "openFolderChooserDialogSAF: " + from_preferences); + this.saf_dialog_from_preferences = from_preferences; + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + //Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + //intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(intent, 42); + } + + /** Call when the SAF save history has been updated. + * This is only public so we can call from testing. + * @param save_folder The new SAF save folder Uri. + */ + public void updateFolderHistorySAF(String save_folder) { + if( MyDebug.LOG ) + Log.d(TAG, "updateSaveHistorySAF"); + if( save_location_history_saf == null ) { + save_location_history_saf = new SaveLocationHistory(this, "save_location_history_saf", save_folder); + } + save_location_history_saf.updateFolderHistory(save_folder, true); + } + + /** Listens for the response from the Storage Access Framework dialog to select a folder + * (as opened with openFolderChooserDialogSAF()). + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if( MyDebug.LOG ) + Log.d(TAG, "onActivityResult: " + requestCode); + if( requestCode == 42 ) { + if( resultCode == RESULT_OK && resultData != null ) { + Uri treeUri = resultData.getData(); + if( MyDebug.LOG ) + Log.d(TAG, "returned treeUri: " + treeUri); + // from https://developer.android.com/guide/topics/providers/document-provider.html#permissions : + final int takeFlags = resultData.getFlags() + & (Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + // Check for the freshest data. + getContentResolver().takePersistableUriPermission(treeUri, takeFlags); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), treeUri.toString()); + editor.apply(); + + if( MyDebug.LOG ) + Log.d(TAG, "update folder history for saf"); + updateFolderHistorySAF(treeUri.toString()); + + File file = applicationInterface.getStorageUtils().getImageFolder(); + if( file != null ) { + preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + file.getAbsolutePath()); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "SAF dialog cancelled"); + // cancelled - if the user had yet to set a save location, make sure we switch SAF back off + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String uri = sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + if( uri.length() == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "no SAF save location was set"); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false); + editor.apply(); + preview.showToast(null, R.string.saf_cancelled); + } + } + + if( !saf_dialog_from_preferences ) { + setWindowFlagsForCamera(); + showPreview(true); + } + } + } + + void updateSaveFolder(String new_save_location) { + if( MyDebug.LOG ) + Log.d(TAG, "updateSaveFolder: " + new_save_location); + if( new_save_location != null ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String orig_save_location = this.applicationInterface.getStorageUtils().getSaveLocation(); + + if( !orig_save_location.equals(new_save_location) ) { + if( MyDebug.LOG ) + Log.d(TAG, "changed save_folder to: " + this.applicationInterface.getStorageUtils().getSaveLocation()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), new_save_location); + editor.apply(); + + this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), true); + this.preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + this.applicationInterface.getStorageUtils().getSaveLocation()); + } + } + } + + public static class MyFolderChooserDialog extends FolderChooserDialog { + @Override + public void onDismiss(DialogInterface dialog) { + if( MyDebug.LOG ) + Log.d(TAG, "FolderChooserDialog dismissed"); + // n.b., fragments have to be static (as they might be inserted into a new Activity - see http://stackoverflow.com/questions/15571010/fragment-inner-class-should-be-static), + // so we access the MainActivity via the fragment's getActivity(). + MainActivity main_activity = (MainActivity)this.getActivity(); + main_activity.setWindowFlagsForCamera(); + main_activity.showPreview(true); + String new_save_location = this.getChosenFolder(); + main_activity.updateSaveFolder(new_save_location); + super.onDismiss(dialog); + } + } + + /** Opens Open Camera's own (non-Storage Access Framework) dialog to select a folder. + */ + private void openFolderChooserDialog() { + if( MyDebug.LOG ) + Log.d(TAG, "openFolderChooserDialog"); + showPreview(false); + setWindowFlagsForSettings(); + FolderChooserDialog fragment = new MyFolderChooserDialog(); + fragment.show(getFragmentManager(), "FOLDER_FRAGMENT"); + } + + /** User can long-click on gallery to select a recent save location from the history, of if not available, + * go straight to the file dialog to pick a folder. + */ + private void longClickedGallery() { + if( MyDebug.LOG ) + Log.d(TAG, "longClickedGallery"); + if( applicationInterface.getStorageUtils().isUsingSAF() ) { + if( save_location_history_saf == null || save_location_history_saf.size() <= 1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "go straight to choose folder dialog for SAF"); + openFolderChooserDialogSAF(false); + return; + } + } + else { + if( save_location_history.size() <= 1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "go straight to choose folder dialog"); + openFolderChooserDialog(); + return; + } + } + + final SaveLocationHistory history = applicationInterface.getStorageUtils().isUsingSAF() ? save_location_history_saf : save_location_history; + showPreview(false); + AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); + alertDialog.setTitle(R.string.choose_save_location); + CharSequence [] items = new CharSequence[history.size()+2]; + int index=0; + // history is stored in order most-recent-last + for(int i=0;i= 0 && which < history.size() ) { + String save_folder = history.get(history.size() - 1 - which); + if( MyDebug.LOG ) + Log.d(TAG, "changed save_folder from history to: " + save_folder); + String save_folder_name = save_folder; + if( applicationInterface.getStorageUtils().isUsingSAF() ) { + // try to get human readable form if possible + File file = applicationInterface.getStorageUtils().getFileFromDocumentUriSAF(Uri.parse(save_folder), true); + if( file != null ) { + save_folder_name = file.getAbsolutePath(); + } + } + preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + if( applicationInterface.getStorageUtils().isUsingSAF() ) + editor.putString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), save_folder); + else + editor.putString(PreferenceKeys.getSaveLocationPreferenceKey(), save_folder); + editor.apply(); + history.updateFolderHistory(save_folder, true); // to move new selection to most recent + } + setWindowFlagsForCamera(); + showPreview(true); + } + } + }); + alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface arg0) { + setWindowFlagsForCamera(); + showPreview(true); + } + }); + alertDialog.show(); + //getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + setWindowFlagsForSettings(); + } + + /** Clears the non-SAF folder history. + */ + public void clearFolderHistory() { + if( MyDebug.LOG ) + Log.d(TAG, "clearFolderHistory"); + save_location_history.clearFolderHistory(getStorageUtils().getSaveLocation()); + } + + /** Clears the SAF folder history. + */ + public void clearFolderHistorySAF() { + if( MyDebug.LOG ) + Log.d(TAG, "clearFolderHistorySAF"); + save_location_history_saf.clearFolderHistory(getStorageUtils().getSaveLocationSAF()); + } + + static private void putBundleExtra(Bundle bundle, String key, List values) { + if( values != null ) { + String [] values_arr = new String[values.size()]; + int i=0; + for(String value: values) { + values_arr[i] = value; + i++; + } + bundle.putStringArray(key, values_arr); + } + } + + public void clickedShare(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedShare"); + applicationInterface.shareLastImage(); + } + + public void clickedTrash(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "clickedTrash"); + applicationInterface.trashLastImage(); + } + + public void takePicture() { + if( MyDebug.LOG ) + Log.d(TAG, "takePicture"); + closePopup(); + this.preview.takePicturePressed(); + } + + /** Lock the screen - this is Open Camera's own lock to guard against accidental presses, + * not the standard Android lock. + */ + void lockScreen() { + findViewById(R.id.locker).setOnTouchListener(new View.OnTouchListener() { + @SuppressLint("ClickableViewAccessibility") @Override + public boolean onTouch(View arg0, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + //return true; + } + }); + screen_is_locked = true; + } + + /** Unlock the screen (see lockScreen()). + */ + void unlockScreen() { + findViewById(R.id.locker).setOnTouchListener(null); + screen_is_locked = false; + } + + /** Whether the screen is locked (see lockScreen()). + */ + public boolean isScreenLocked() { + return screen_is_locked; + } + + /** Listen for gestures. + * Doing a swipe will unlock the screen (see lockScreen()). + */ + private class MyGestureDetector extends SimpleOnGestureListener { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + try { + if( MyDebug.LOG ) + Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY()); + final ViewConfiguration vc = ViewConfiguration.get(MainActivity.this); + //final int swipeMinDistance = 4*vc.getScaledPagingTouchSlop(); + final float scale = getResources().getDisplayMetrics().density; + final int swipeMinDistance = (int) (160 * scale + 0.5f); // convert dps to pixels + final int swipeThresholdVelocity = vc.getScaledMinimumFlingVelocity(); + if( MyDebug.LOG ) { + Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY()); + Log.d(TAG, "swipeMinDistance: " + swipeMinDistance); + } + float xdist = e1.getX() - e2.getX(); + float ydist = e1.getY() - e2.getY(); + float dist2 = xdist*xdist + ydist*ydist; + float vel2 = velocityX*velocityX + velocityY*velocityY; + if( dist2 > swipeMinDistance*swipeMinDistance && vel2 > swipeThresholdVelocity*swipeThresholdVelocity ) { + preview.showToast(screen_locked_toast, R.string.unlocked); + unlockScreen(); + } + } + catch(Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public boolean onDown(MotionEvent e) { + preview.showToast(screen_locked_toast, R.string.screen_is_locked); + return true; + } + } + + @Override + protected void onSaveInstanceState(Bundle state) { + if( MyDebug.LOG ) + Log.d(TAG, "onSaveInstanceState"); + super.onSaveInstanceState(state); + if( this.preview != null ) { + preview.onSaveInstanceState(state); + } + if( this.applicationInterface != null ) { + applicationInterface.onSaveInstanceState(state); + } + } + + public boolean supportsExposureButton() { + if( preview.getCameraController() == null ) + return false; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String iso_value = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), preview.getCameraController().getDefaultISO()); + boolean manual_iso = !iso_value.equals("auto"); + return preview.supportsExposures() || (manual_iso && preview.supportsISORange() ); + } + + void cameraSetup() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "cameraSetup"); + debug_time = System.currentTimeMillis(); + } + if( this.supportsForceVideo4K() && preview.usingCamera2API() ) { + if( MyDebug.LOG ) + Log.d(TAG, "using Camera2 API, so can disable the force 4K option"); + this.disableForceVideo4K(); + } + if( this.supportsForceVideo4K() && preview.getVideoQualityHander().getSupportedVideoSizes() != null ) { + for(CameraController.Size size : preview.getVideoQualityHander().getSupportedVideoSizes()) { + if( size.width >= 3840 && size.height >= 2160 ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera natively supports 4K, so can disable the force option"); + this.disableForceVideo4K(); + } + } + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after handling Force 4K option: " + (System.currentTimeMillis() - debug_time)); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + { + if( MyDebug.LOG ) + Log.d(TAG, "set up zoom"); + if( MyDebug.LOG ) + Log.d(TAG, "has_zoom? " + preview.supportsZoom()); + ZoomControls zoomControls = (ZoomControls) findViewById(R.id.zoom); + SeekBar zoomSeekBar = (SeekBar) findViewById(R.id.zoom_seekbar); + + if( preview.supportsZoom() ) { + if( sharedPreferences.getBoolean(PreferenceKeys.getShowZoomControlsPreferenceKey(), false) ) { + zoomControls.setIsZoomInEnabled(true); + zoomControls.setIsZoomOutEnabled(true); + zoomControls.setZoomSpeed(20); + + zoomControls.setOnZoomInClickListener(new View.OnClickListener(){ + public void onClick(View v){ + zoomIn(); + } + }); + zoomControls.setOnZoomOutClickListener(new View.OnClickListener(){ + public void onClick(View v){ + zoomOut(); + } + }); + if( !mainUI.inImmersiveMode() ) { + zoomControls.setVisibility(View.VISIBLE); + } + } + else { + zoomControls.setVisibility(View.INVISIBLE); // must be INVISIBLE not GONE, so we can still position the zoomSeekBar relative to it + } + + zoomSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state + zoomSeekBar.setMax(preview.getMaxZoom()); + zoomSeekBar.setProgress(preview.getMaxZoom()-preview.getCameraController().getZoom()); + zoomSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if( MyDebug.LOG ) + Log.d(TAG, "zoom onProgressChanged: " + progress); + preview.zoomTo(preview.getMaxZoom()-progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + if( sharedPreferences.getBoolean(PreferenceKeys.getShowZoomSliderControlsPreferenceKey(), true) ) { + if( !mainUI.inImmersiveMode() ) { + zoomSeekBar.setVisibility(View.VISIBLE); + } + } + else { + zoomSeekBar.setVisibility(View.INVISIBLE); + } + } + else { + zoomControls.setVisibility(View.GONE); + zoomSeekBar.setVisibility(View.GONE); + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting up zoom: " + (System.currentTimeMillis() - debug_time)); + + View takePhotoButton = findViewById(R.id.take_photo); + if( sharedPreferences.getBoolean(PreferenceKeys.getShowTakePhotoPreferenceKey(), true) ) { + if( !mainUI.inImmersiveMode() ) { + takePhotoButton.setVisibility(View.VISIBLE); + } + } + else { + takePhotoButton.setVisibility(View.INVISIBLE); + } + } + { + if( MyDebug.LOG ) + Log.d(TAG, "set up manual focus"); + SeekBar focusSeekBar = (SeekBar) findViewById(R.id.focus_seekbar); + focusSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state + setProgressSeekbarScaled(focusSeekBar, 0.0, preview.getMinimumFocusDistance(), preview.getCameraController().getFocusDistance()); + focusSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + double frac = progress/100.0; + double scaling = MainActivity.seekbarScaling(frac); + float focus_distance = (float)(scaling * preview.getMinimumFocusDistance()); + preview.setFocusDistance(focus_distance); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + final int visibility = preview.getCurrentFocusValue() != null && this.getPreview().getCurrentFocusValue().equals("focus_mode_manual2") ? View.VISIBLE : View.INVISIBLE; + focusSeekBar.setVisibility(visibility); + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting up manual focus: " + (System.currentTimeMillis() - debug_time)); + { + if( preview.supportsISORange()) { + if( MyDebug.LOG ) + Log.d(TAG, "set up iso"); + SeekBar iso_seek_bar = ((SeekBar)findViewById(R.id.iso_seekbar)); + iso_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state + setProgressSeekbarScaled(iso_seek_bar, preview.getMinimumISO(), preview.getMaximumISO(), preview.getCameraController().getISO()); + iso_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if( MyDebug.LOG ) + Log.d(TAG, "iso seekbar onProgressChanged: " + progress); + double frac = progress/100.0; + if( MyDebug.LOG ) + Log.d(TAG, "exposure_time frac: " + frac); + double scaling = MainActivity.seekbarScaling(frac); + if( MyDebug.LOG ) + Log.d(TAG, "exposure_time scaling: " + scaling); + int min_iso = preview.getMinimumISO(); + int max_iso = preview.getMaximumISO(); + int iso = min_iso + (int)(scaling * (max_iso - min_iso)); + preview.setISO(iso); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + if( preview.supportsExposureTime() ) { + if( MyDebug.LOG ) + Log.d(TAG, "set up exposure time"); + SeekBar exposure_time_seek_bar = ((SeekBar)findViewById(R.id.exposure_time_seekbar)); + exposure_time_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state + setProgressSeekbarScaled(exposure_time_seek_bar, preview.getMinimumExposureTime(), preview.getMaximumExposureTime(), preview.getCameraController().getExposureTime()); + exposure_time_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if( MyDebug.LOG ) + Log.d(TAG, "exposure_time seekbar onProgressChanged: " + progress); + double frac = progress/100.0; + if( MyDebug.LOG ) + Log.d(TAG, "exposure_time frac: " + frac); + //long exposure_time = min_exposure_time + (long)(frac * (max_exposure_time - min_exposure_time)); + //double exposure_time_r = min_exposure_time_r + (frac * (max_exposure_time_r - min_exposure_time_r)); + //long exposure_time = (long)(1.0 / exposure_time_r); + // we use the formula: [100^(percent/100) - 1]/99.0 rather than a simple linear scaling + double scaling = MainActivity.seekbarScaling(frac); + if( MyDebug.LOG ) + Log.d(TAG, "exposure_time scaling: " + scaling); + long min_exposure_time = preview.getMinimumExposureTime(); + long max_exposure_time = preview.getMaximumExposureTime(); + long exposure_time = min_exposure_time + (long)(scaling * (max_exposure_time - min_exposure_time)); + preview.setExposureTime(exposure_time); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + } + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting up iso: " + (System.currentTimeMillis() - debug_time)); + { + if( preview.supportsExposures() ) { + if( MyDebug.LOG ) + Log.d(TAG, "set up exposure compensation"); + final int min_exposure = preview.getMinimumExposure(); + SeekBar exposure_seek_bar = ((SeekBar)findViewById(R.id.exposure_seekbar)); + exposure_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state + exposure_seek_bar.setMax( preview.getMaximumExposure() - min_exposure ); + exposure_seek_bar.setProgress( preview.getCurrentExposure() - min_exposure ); + exposure_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if( MyDebug.LOG ) + Log.d(TAG, "exposure seekbar onProgressChanged: " + progress); + preview.setExposure(min_exposure + progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + ZoomControls seek_bar_zoom = (ZoomControls)findViewById(R.id.exposure_seekbar_zoom); + seek_bar_zoom.setOnZoomInClickListener(new View.OnClickListener(){ + public void onClick(View v){ + changeExposure(1); + } + }); + seek_bar_zoom.setOnZoomOutClickListener(new View.OnClickListener(){ + public void onClick(View v){ + changeExposure(-1); + } + }); + } + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting up exposure: " + (System.currentTimeMillis() - debug_time)); + + View exposureButton = findViewById(R.id.exposure); + exposureButton.setVisibility(supportsExposureButton() && !mainUI.inImmersiveMode() ? View.VISIBLE : View.GONE); + + ImageButton exposureLockButton = (ImageButton) findViewById(R.id.exposure_lock); + exposureLockButton.setVisibility(preview.supportsExposureLock() && !mainUI.inImmersiveMode() ? View.VISIBLE : View.GONE); + if( preview.supportsExposureLock() ) { + exposureLockButton.setImageResource(preview.isExposureLocked() ? R.drawable.exposure_locked : R.drawable.exposure_unlocked); + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting exposure lock button: " + (System.currentTimeMillis() - debug_time)); + + mainUI.setPopupIcon(); // needed so that the icon is set right even if no flash mode is set when starting up camera (e.g., switching to front camera with no flash) + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting popup icon: " + (System.currentTimeMillis() - debug_time)); + + mainUI.setTakePhotoIcon(); + mainUI.setSwitchCameraContentDescription(); + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: time after setting take photo icon: " + (System.currentTimeMillis() - debug_time)); + + if( !block_startup_toast ) { + this.showPhotoVideoToast(false); + } + if( MyDebug.LOG ) + Log.d(TAG, "cameraSetup: total time for cameraSetup: " + (System.currentTimeMillis() - debug_time)); + } + + public boolean supportsAutoStabilise() { + return this.supports_auto_stabilise; + } + + public boolean supportsDRO() { + // require at least Android 5, for the Renderscript support in HDRProcessor + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) + return true; + return false; + } + + public boolean supportsHDR() { + // we also require the device have sufficient memory to do the processing, simplest to use the same test as we do for auto-stabilise... + if( this.supportsAutoStabilise() && preview.supportsExpoBracketing() ) + return true; + return false; + } + + public boolean supportsExpoBracketing() { + if( preview.supportsExpoBracketing() ) + return true; + return false; + } + + public boolean supportsForceVideo4K() { + return this.supports_force_video_4k; + } + + public boolean supportsCamera2() { + return this.supports_camera2; + } + + private void disableForceVideo4K() { + this.supports_force_video_4k = false; + } + + /** Return free memory in MB. + */ + @SuppressWarnings("deprecation") + public long freeMemory() { // return free memory in MB + try { + File folder = applicationInterface.getStorageUtils().getImageFolder(); + if( folder == null ) { + throw new IllegalArgumentException(); // so that we fall onto the backup + } + StatFs statFs = new StatFs(folder.getAbsolutePath()); + // cast to long to avoid overflow! + long blocks = statFs.getAvailableBlocks(); + long size = statFs.getBlockSize(); + return (blocks*size) / 1048576; + } + catch(IllegalArgumentException e) { + // this can happen if folder doesn't exist, or don't have read access + // if the save folder is a subfolder of DCIM, we can just use that instead + try { + if( !applicationInterface.getStorageUtils().isUsingSAF() ) { + // StorageUtils.getSaveLocation() only valid if !isUsingSAF() + String folder_name = applicationInterface.getStorageUtils().getSaveLocation(); + if( !folder_name.startsWith("/") ) { + File folder = StorageUtils.getBaseFolder(); + StatFs statFs = new StatFs(folder.getAbsolutePath()); + // cast to long to avoid overflow! + long blocks = statFs.getAvailableBlocks(); + long size = statFs.getBlockSize(); + return (blocks*size) / 1048576; + } + } + } + catch(IllegalArgumentException e2) { + // just in case + } + } + return -1; + } + + public static String getDonateLink() { + return "https://play.google.com/store/apps/details?id=harman.mark.donation"; + } + + /*public static String getDonateMarketLink() { + return "market://details?id=harman.mark.donation"; + }*/ + + public Preview getPreview() { + return this.preview; + } + + public MainUI getMainUI() { + return this.mainUI; + } + + public MyApplicationInterface getApplicationInterface() { + return this.applicationInterface; + } + + public TextFormatter getTextFormatter() { + return this.textFormatter; + } + + public LocationSupplier getLocationSupplier() { + return this.applicationInterface.getLocationSupplier(); + } + + public StorageUtils getStorageUtils() { + return this.applicationInterface.getStorageUtils(); + } + + public File getImageFolder() { + return this.applicationInterface.getStorageUtils().getImageFolder(); + } + + public ToastBoxer getChangedAutoStabiliseToastBoxer() { + return changed_auto_stabilise_toast; + } + + /** Displays a toast with information about the current preferences. + * If always_show is true, the toast is always displayed; otherwise, we only display + * a toast if it's important to notify the user (i.e., unusual non-default settings are + * set). We want a balance between not pestering the user too much, whilst also reminding + * them if certain settings are on. + */ + private void showPhotoVideoToast(boolean always_show) { + if( MyDebug.LOG ) { + Log.d(TAG, "showPhotoVideoToast"); + Log.d(TAG, "always_show? " + always_show); + } + CameraController camera_controller = preview.getCameraController(); + if( camera_controller == null || this.camera_in_background ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not open or in background"); + return; + } + String toast_string; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean simple = true; + if( preview.isVideo() ) { + CamcorderProfile profile = preview.getCamcorderProfile(); + String bitrate_string; + if( profile.videoBitRate >= 10000000 ) + bitrate_string = profile.videoBitRate/1000000 + "Mbps"; + else if( profile.videoBitRate >= 10000 ) + bitrate_string = profile.videoBitRate/1000 + "Kbps"; + else + bitrate_string = profile.videoBitRate + "bps"; + + toast_string = getResources().getString(R.string.video) + ": " + profile.videoFrameWidth + "x" + profile.videoFrameHeight + ", " + profile.videoFrameRate + "fps, " + bitrate_string; + boolean record_audio = sharedPreferences.getBoolean(PreferenceKeys.getRecordAudioPreferenceKey(), true); + if( !record_audio ) { + toast_string += "\n" + getResources().getString(R.string.audio_disabled); + simple = false; + } + String max_duration_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "0"); + if( max_duration_value.length() > 0 && !max_duration_value.equals("0") ) { + String [] entries_array = getResources().getStringArray(R.array.preference_video_max_duration_entries); + String [] values_array = getResources().getStringArray(R.array.preference_video_max_duration_values); + int index = Arrays.asList(values_array).indexOf(max_duration_value); + if( index != -1 ) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + getResources().getString(R.string.max_duration) +": " + entry; + simple = false; + } + } + long max_filesize = applicationInterface.getVideoMaxFileSizeUserPref(); + if( max_filesize != 0 ) { + long max_filesize_mb = max_filesize/(1024*1024); + toast_string += "\n" + getResources().getString(R.string.max_filesize) +": " + max_filesize_mb + getResources().getString(R.string.mb_abbreviation); + simple = false; + } + if( sharedPreferences.getBoolean(PreferenceKeys.getVideoFlashPreferenceKey(), false) && preview.supportsFlash() ) { + toast_string += "\n" + getResources().getString(R.string.preference_video_flash); + simple = false; + } + } + else { + toast_string = getResources().getString(R.string.photo); + CameraController.Size current_size = preview.getCurrentPictureSize(); + toast_string += " " + current_size.width + "x" + current_size.height; + if( preview.supportsFocus() && preview.getSupportedFocusValues().size() > 1 ) { + String focus_value = preview.getCurrentFocusValue(); + if( focus_value != null && !focus_value.equals("focus_mode_auto") && !focus_value.equals("focus_mode_continuous_picture") ) { + String focus_entry = preview.findFocusEntryForValue(focus_value); + if( focus_entry != null ) { + toast_string += "\n" + focus_entry; + } + } + } + if( sharedPreferences.getBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), false) ) { + // important as users are sometimes confused at the behaviour if they don't realise the option is on + toast_string += "\n" + getResources().getString(R.string.preference_auto_stabilise); + simple = false; + } + String photo_mode_string = null; + MyApplicationInterface.PhotoMode photo_mode = applicationInterface.getPhotoMode(); + if( photo_mode == MyApplicationInterface.PhotoMode.DRO ) { + photo_mode_string = getResources().getString(R.string.photo_mode_dro); + } + else if( photo_mode == MyApplicationInterface.PhotoMode.HDR ) { + photo_mode_string = getResources().getString(R.string.photo_mode_hdr); + } + else if( photo_mode == MyApplicationInterface.PhotoMode.ExpoBracketing ) { + photo_mode_string = getResources().getString(R.string.photo_mode_expo_bracketing_full); + } + if( photo_mode_string != null ) { + toast_string += "\n" + getResources().getString(R.string.photo_mode) + ": " + photo_mode_string; + simple = false; + } + } + if( applicationInterface.getFaceDetectionPref() ) { + // important so that the user realises why touching for focus/metering areas won't work - easy to forget that face detection has been turned on! + toast_string += "\n" + getResources().getString(R.string.preference_face_detection); + simple = false; + } + String iso_value = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), camera_controller.getDefaultISO()); + if( !iso_value.equals(camera_controller.getDefaultISO()) ) { + toast_string += "\nISO: " + iso_value; + if( preview.supportsExposureTime() ) { + long exposure_time_value = sharedPreferences.getLong(PreferenceKeys.getExposureTimePreferenceKey(), camera_controller.getDefaultExposureTime()); + toast_string += " " + preview.getExposureTimeString(exposure_time_value); + } + simple = false; + } + int current_exposure = camera_controller.getExposureCompensation(); + if( current_exposure != 0 ) { + toast_string += "\n" + preview.getExposureCompensationString(current_exposure); + simple = false; + } + String scene_mode = camera_controller.getSceneMode(); + if( scene_mode != null && !scene_mode.equals(camera_controller.getDefaultSceneMode()) ) { + toast_string += "\n" + getResources().getString(R.string.scene_mode) + ": " + scene_mode; + simple = false; + } + String white_balance = camera_controller.getWhiteBalance(); + if( white_balance != null && !white_balance.equals(camera_controller.getDefaultWhiteBalance()) ) { + toast_string += "\n" + getResources().getString(R.string.white_balance) + ": " + white_balance; + simple = false; + } + String color_effect = camera_controller.getColorEffect(); + if( color_effect != null && !color_effect.equals(camera_controller.getDefaultColorEffect()) ) { + toast_string += "\n" + getResources().getString(R.string.color_effect) + ": " + color_effect; + simple = false; + } + String lock_orientation = sharedPreferences.getString(PreferenceKeys.getLockOrientationPreferenceKey(), "none"); + if( !lock_orientation.equals("none") ) { + String [] entries_array = getResources().getStringArray(R.array.preference_lock_orientation_entries); + String [] values_array = getResources().getStringArray(R.array.preference_lock_orientation_values); + int index = Arrays.asList(values_array).indexOf(lock_orientation); + if( index != -1 ) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + entry; + simple = false; + } + } + String timer = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + if( !timer.equals("0") ) { + String [] entries_array = getResources().getStringArray(R.array.preference_timer_entries); + String [] values_array = getResources().getStringArray(R.array.preference_timer_values); + int index = Arrays.asList(values_array).indexOf(timer); + if( index != -1 ) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + getResources().getString(R.string.preference_timer) + ": " + entry; + simple = false; + } + } + String repeat = applicationInterface.getRepeatPref(); + if( !repeat.equals("1") ) { + String [] entries_array = getResources().getStringArray(R.array.preference_burst_mode_entries); + String [] values_array = getResources().getStringArray(R.array.preference_burst_mode_values); + int index = Arrays.asList(values_array).indexOf(repeat); + if( index != -1 ) { // just in case! + String entry = entries_array[index]; + toast_string += "\n" + getResources().getString(R.string.preference_burst_mode) + ": " + entry; + simple = false; + } + } + /*if( audio_listener != null ) { + toast_string += "\n" + getResources().getString(R.string.preference_audio_noise_control); + }*/ + + if( MyDebug.LOG ) { + Log.d(TAG, "toast_string: " + toast_string); + Log.d(TAG, "simple?: " + simple); + } + if( !simple || always_show ) + preview.showToast(switch_video_toast, toast_string); + } + + private void freeAudioListener(boolean wait_until_done) { + if( MyDebug.LOG ) + Log.d(TAG, "freeAudioListener"); + if( audio_listener != null ) { + audio_listener.release(wait_until_done); + audio_listener = null; + } + mainUI.audioControlStopped(); + } + + private void startAudioListener() { + if( MyDebug.LOG ) + Log.d(TAG, "startAudioListener"); + audio_listener = new AudioListener(this); + if( audio_listener.status() ) { + audio_listener.start(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String sensitivity_pref = sharedPreferences.getString(PreferenceKeys.getAudioNoiseControlSensitivityPreferenceKey(), "0"); + if( sensitivity_pref.equals("3") ) { + audio_noise_sensitivity = 50; + } + else if( sensitivity_pref.equals("2") ) { + audio_noise_sensitivity = 75; + } + else if( sensitivity_pref.equals("1") ) { + audio_noise_sensitivity = 125; + } + else if( sensitivity_pref.equals("-1") ) { + audio_noise_sensitivity = 150; + } + else if( sensitivity_pref.equals("-2") ) { + audio_noise_sensitivity = 200; + } + else { + // default + audio_noise_sensitivity = 100; + } + mainUI.audioControlStarted(); + } + else { + audio_listener.release(true); // shouldn't be needed, but just to be safe + audio_listener = null; + preview.showToast(null, R.string.audio_listener_failed); + } + } + + private void initSpeechRecognizer() { + if( MyDebug.LOG ) + Log.d(TAG, "initSpeechRecognizer"); + // in theory we could create the speech recognizer always (hopefully it shouldn't use battery when not listening?), though to be safe, we only do this when the option is enabled (e.g., just in case this doesn't work on some devices!) + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean want_speech_recognizer = sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none").equals("voice"); + if( speechRecognizer == null && want_speech_recognizer ) { + if( MyDebug.LOG ) + Log.d(TAG, "create new speechRecognizer"); + speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this); + if( speechRecognizer != null ) { + speechRecognizerIsStarted = false; + speechRecognizer.setRecognitionListener(new RecognitionListener() { + @Override + public void onBeginningOfSpeech() { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onBeginningOfSpeech"); + } + + @Override + public void onBufferReceived(byte[] buffer) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onBufferReceived"); + } + + @Override + public void onEndOfSpeech() { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onEndOfSpeech"); + speechRecognizerStopped(); + } + + @Override + public void onError(int error) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onError: " + error); + if( error != SpeechRecognizer.ERROR_NO_MATCH ) { + // we sometime receive ERROR_NO_MATCH straight after listening starts + // it seems that the end is signalled either by ERROR_SPEECH_TIMEOUT or onEndOfSpeech() + speechRecognizerStopped(); + } + } + + @Override + public void onEvent(int eventType, Bundle params) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onEvent"); + } + + @Override + public void onPartialResults(Bundle partialResults) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onPartialResults"); + } + + @Override + public void onReadyForSpeech(Bundle params) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onReadyForSpeech"); + } + + public void onResults(Bundle results) { + if( MyDebug.LOG ) + Log.d(TAG, "RecognitionListener: onResults"); + speechRecognizerStopped(); + ArrayList list = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + boolean found = false; + final String trigger = "cheese"; + //String debug_toast = ""; + for(int i=0;list != null && i 0 ) + debug_toast += "\n"; + debug_toast += text + " : " + results.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES)[i];*/ + if( text.toLowerCase(Locale.US).contains(trigger) ) { + found = true; + } + } + //preview.showToast(null, debug_toast); // debug only! + if( found ) { + if( MyDebug.LOG ) + Log.d(TAG, "audio trigger from speech recognition"); + audioTrigger(); + } + else if( list != null && list.size() > 0 ) { + String toast = list.get(0) + "?"; + if( MyDebug.LOG ) + Log.d(TAG, "unrecognised: " + toast); + preview.showToast(audio_control_toast, toast); + } + } + + @Override + public void onRmsChanged(float rmsdB) { + } + }); + if( !mainUI.inImmersiveMode() ) { + View speechRecognizerButton = findViewById(R.id.audio_control); + speechRecognizerButton.setVisibility(View.VISIBLE); + } + } + } + else if( speechRecognizer != null && !want_speech_recognizer ) { + if( MyDebug.LOG ) + Log.d(TAG, "free existing SpeechRecognizer"); + freeSpeechRecognizer(); + } + } + + private void freeSpeechRecognizer() { + if( MyDebug.LOG ) + Log.d(TAG, "freeSpeechRecognizer"); + if( speechRecognizer != null ) { + speechRecognizerStopped(); + View speechRecognizerButton = findViewById(R.id.audio_control); + speechRecognizerButton.setVisibility(View.GONE); + speechRecognizer.destroy(); + speechRecognizer = null; + } + } + + public boolean hasAudioControl() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + String audio_control = sharedPreferences.getString(PreferenceKeys.getAudioControlPreferenceKey(), "none"); + if( audio_control.equals("voice") ) { + return speechRecognizer != null; + } + else if( audio_control.equals("noise") ) { + return true; + } + return false; + } + + /*void startAudioListeners() { + initAudioListener(); + // no need to restart speech recognizer, as we didn't free it in stopAudioListeners(), and it's controlled by a user button + }*/ + + public void stopAudioListeners() { + freeAudioListener(true); + if( speechRecognizer != null ) { + // no need to free the speech recognizer, just stop it + speechRecognizer.stopListening(); + speechRecognizerStopped(); + } + } + + private void initLocation() { + if( MyDebug.LOG ) + Log.d(TAG, "initLocation"); + if( !applicationInterface.getLocationSupplier().setupLocationListener() ) { + if( MyDebug.LOG ) + Log.d(TAG, "location permission not available, so request permission"); + requestLocationPermission(); + } + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void initSound() { + if( sound_pool == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "create new sound_pool"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + AudioAttributes audio_attributes = new AudioAttributes.Builder() + .setLegacyStreamType(AudioManager.STREAM_SYSTEM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + sound_pool = new SoundPool.Builder() + .setMaxStreams(1) + .setAudioAttributes(audio_attributes) + .build(); + } + else { + sound_pool = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); + } + sound_ids = new SparseIntArray(); + } + } + + private void releaseSound() { + if( sound_pool != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "release sound_pool"); + sound_pool.release(); + sound_pool = null; + sound_ids = null; + } + } + + // must be called before playSound (allowing enough time to load the sound) + private void loadSound(int resource_id) { + if( sound_pool != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "loading sound resource: " + resource_id); + int sound_id = sound_pool.load(this, resource_id, 1); + if( MyDebug.LOG ) + Log.d(TAG, " loaded sound: " + sound_id); + sound_ids.put(resource_id, sound_id); + } + } + + // must call loadSound first (allowing enough time to load the sound) + void playSound(int resource_id) { + if( sound_pool != null ) { + if( sound_ids.indexOfKey(resource_id) < 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "resource not loaded: " + resource_id); + } + else { + int sound_id = sound_ids.get(resource_id); + if( MyDebug.LOG ) + Log.d(TAG, "play sound: " + sound_id); + sound_pool.play(sound_id, 1.0f, 1.0f, 0, 0, 1); + } + } + } + + @SuppressWarnings("deprecation") + void speak(String text) { + if( textToSpeech != null && textToSpeechSuccess ) { + textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null); + } + } + + // Android 6+ permission handling: + + final private int MY_PERMISSIONS_REQUEST_CAMERA = 0; + final private int MY_PERMISSIONS_REQUEST_STORAGE = 1; + final private int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 2; + final private int MY_PERMISSIONS_REQUEST_LOCATION = 3; + + /** Show a "rationale" to the user for needing a particular permission, then request that permission again + * once they close the dialog. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void showRequestPermissionRationale(final int permission_code) { + if( MyDebug.LOG ) + Log.d(TAG, "showRequestPermissionRational: " + permission_code); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + boolean ok = true; + String [] permissions = null; + int message_id = 0; + if( permission_code == MY_PERMISSIONS_REQUEST_CAMERA ) { + if( MyDebug.LOG ) + Log.d(TAG, "display rationale for camera permission"); + permissions = new String[]{Manifest.permission.CAMERA}; + message_id = R.string.permission_rationale_camera; + } + else if( permission_code == MY_PERMISSIONS_REQUEST_STORAGE ) { + if( MyDebug.LOG ) + Log.d(TAG, "display rationale for storage permission"); + permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; + message_id = R.string.permission_rationale_storage; + } + else if( permission_code == MY_PERMISSIONS_REQUEST_RECORD_AUDIO ) { + if( MyDebug.LOG ) + Log.d(TAG, "display rationale for record audio permission"); + permissions = new String[]{Manifest.permission.RECORD_AUDIO}; + message_id = R.string.permission_rationale_record_audio; + } + else if( permission_code == MY_PERMISSIONS_REQUEST_LOCATION ) { + if( MyDebug.LOG ) + Log.d(TAG, "display rationale for location permission"); + permissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}; + message_id = R.string.permission_rationale_location; + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "showRequestPermissionRational unknown permission_code: " + permission_code); + ok = false; + } + + if( ok ) { + final String [] permissions_f = permissions; + new AlertDialog.Builder(this) + .setTitle(R.string.permission_rationale_title) + .setMessage(message_id) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, null) + .setOnDismissListener(new OnDismissListener() { + public void onDismiss(DialogInterface dialog) { + if( MyDebug.LOG ) + Log.d(TAG, "requesting permission..."); + ActivityCompat.requestPermissions(MainActivity.this, permissions_f, permission_code); + } + }).show(); + } + } + + void requestCameraPermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestCameraPermission"); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + if( ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) ) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + showRequestPermissionRationale(MY_PERMISSIONS_REQUEST_CAMERA); + } + else { + // Can go ahead and request the permission + if( MyDebug.LOG ) + Log.d(TAG, "requesting camera permission..."); + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, MY_PERMISSIONS_REQUEST_CAMERA); + } + } + + void requestStoragePermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestStoragePermission"); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + if( ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + showRequestPermissionRationale(MY_PERMISSIONS_REQUEST_STORAGE); + } + else { + // Can go ahead and request the permission + if( MyDebug.LOG ) + Log.d(TAG, "requesting storage permission..."); + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_STORAGE); + } + } + + void requestRecordAudioPermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestRecordAudioPermission"); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + if( ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO) ) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + showRequestPermissionRationale(MY_PERMISSIONS_REQUEST_RECORD_AUDIO); + } + else { + // Can go ahead and request the permission + if( MyDebug.LOG ) + Log.d(TAG, "requesting record audio permission..."); + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_RECORD_AUDIO); + } + } + + private void requestLocationPermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestLocationPermission"); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + if( ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION) || + ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION) ) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + showRequestPermissionRationale(MY_PERMISSIONS_REQUEST_LOCATION); + } + else { + // Can go ahead and request the permission + if( MyDebug.LOG ) + Log.d(TAG, "requesting loacation permissions..."); + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, MY_PERMISSIONS_REQUEST_LOCATION); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + if( MyDebug.LOG ) + Log.d(TAG, "onRequestPermissionsResult: requestCode " + requestCode); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); + return; + } + + switch( requestCode ) { + case MY_PERMISSIONS_REQUEST_CAMERA: + { + // If request is cancelled, the result arrays are empty. + if( grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { + // permission was granted, yay! Do the + // contacts-related task you need to do. + if( MyDebug.LOG ) + Log.d(TAG, "camera permission granted"); + preview.retryOpenCamera(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "camera permission denied"); + // permission denied, boo! Disable the + // functionality that depends on this permission. + // Open Camera doesn't need to do anything: the camera will remain closed + } + return; + } + case MY_PERMISSIONS_REQUEST_STORAGE: + { + // If request is cancelled, the result arrays are empty. + if( grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { + // permission was granted, yay! Do the + // contacts-related task you need to do. + if( MyDebug.LOG ) + Log.d(TAG, "storage permission granted"); + preview.retryOpenCamera(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "storage permission denied"); + // permission denied, boo! Disable the + // functionality that depends on this permission. + // Open Camera doesn't need to do anything: the camera will remain closed + } + return; + } + case MY_PERMISSIONS_REQUEST_RECORD_AUDIO: + { + // If request is cancelled, the result arrays are empty. + if( grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { + // permission was granted, yay! Do the + // contacts-related task you need to do. + if( MyDebug.LOG ) + Log.d(TAG, "record audio permission granted"); + // no need to do anything + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "record audio permission denied"); + // permission denied, boo! Disable the + // functionality that depends on this permission. + // no need to do anything + // note that we don't turn off record audio option, as user may then record video not realising audio won't be recorded - best to be explicit each time + } + return; + } + case MY_PERMISSIONS_REQUEST_LOCATION: + { + // If request is cancelled, the result arrays are empty. + if( grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { + // permission was granted, yay! Do the + // contacts-related task you need to do. + if( MyDebug.LOG ) + Log.d(TAG, "location permission granted"); + initLocation(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "location permission denied"); + // permission denied, boo! Disable the + // functionality that depends on this permission. + // for location, seems best to turn the option back off + if( MyDebug.LOG ) + Log.d(TAG, "location permission not available, so switch location off"); + preview.showToast(null, R.string.permission_location_not_available); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(PreferenceKeys.getLocationPreferenceKey(), false); + editor.apply(); + } + return; + } + default: + { + if( MyDebug.LOG ) + Log.e(TAG, "unknown requestCode " + requestCode); + } + } + } + + // for testing: + public SaveLocationHistory getSaveLocationHistory() { + return this.save_location_history; + } + + public SaveLocationHistory getSaveLocationHistorySAF() { + return this.save_location_history_saf; + } + + public void usedFolderPicker() { + if( applicationInterface.getStorageUtils().isUsingSAF() ) { + save_location_history_saf.updateFolderHistory(getStorageUtils().getSaveLocationSAF(), true); + } + else { + save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true); + } + } + + public boolean hasThumbnailAnimation() { + return this.applicationInterface.hasThumbnailAnimation(); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java new file mode 100644 index 00000000..0fccd2bc --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -0,0 +1,2002 @@ +package net.sourceforge.opencamera; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.Preview.ApplicationInterface; +import net.sourceforge.opencamera.Preview.Preview; +import net.sourceforge.opencamera.UI.DrawPreview; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.hardware.camera2.DngCreator; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.Image; +import android.media.MediaMetadataRetriever; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.preference.PreferenceManager; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.util.Log; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; + +/** Our implementation of ApplicationInterface, see there for details. + */ +public class MyApplicationInterface implements ApplicationInterface { + private static final String TAG = "MyApplicationInterface"; + + // note, okay to change the order of enums in future versions, as getPhotoMode() does not rely on the order for the saved photo mode + public enum PhotoMode { + Standard, + DRO, // single image "fake" HDR + HDR, // HDR created from multiple (expo bracketing) images + ExpoBracketing // take multiple expo bracketed images, without combining to a single image + } + + private final MainActivity main_activity; + private final LocationSupplier locationSupplier; + private final StorageUtils storageUtils; + private final DrawPreview drawPreview; + private final ImageSaver imageSaver; + + private File last_video_file = null; + private Uri last_video_file_saf = null; + + private final Timer subtitleVideoTimer = new Timer(); + private TimerTask subtitleVideoTimerTask; + + private final Rect text_bounds = new Rect(); + private boolean used_front_screen_flash ; + + private boolean last_images_saf; // whether the last images array are using SAF or not + /** This class keeps track of the images saved in this batch, for use with Pause Preview option, so we can share or trash images. + */ + private static class LastImage { + public final boolean share; // one of the images in the list should have share set to true, to indicate which image to share + public final String name; + final Uri uri; + + LastImage(Uri uri, boolean share) { + this.name = null; + this.uri = uri; + this.share = share; + } + + LastImage(String filename, boolean share) { + this.name = filename; + this.uri = Uri.parse("file://" + this.name); + this.share = share; + } + } + private final List last_images = new ArrayList<>(); + + // camera properties which are saved in bundle, but not stored in preferences (so will be remembered if the app goes into background, but not after restart) + private int cameraId = 0; + private int zoom_factor = 0; + private float focus_distance = 0.0f; + + MyApplicationInterface(MainActivity main_activity, Bundle savedInstanceState) { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "MyApplicationInterface"); + debug_time = System.currentTimeMillis(); + } + this.main_activity = main_activity; + this.locationSupplier = new LocationSupplier(main_activity); + if( MyDebug.LOG ) + Log.d(TAG, "MyApplicationInterface: time after creating location supplier: " + (System.currentTimeMillis() - debug_time)); + this.storageUtils = new StorageUtils(main_activity); + if( MyDebug.LOG ) + Log.d(TAG, "MyApplicationInterface: time after creating storage utils: " + (System.currentTimeMillis() - debug_time)); + this.drawPreview = new DrawPreview(main_activity, this); + + this.imageSaver = new ImageSaver(main_activity); + this.imageSaver.start(); + + if( savedInstanceState != null ) { + cameraId = savedInstanceState.getInt("cameraId", 0); + if( MyDebug.LOG ) + Log.d(TAG, "found cameraId: " + cameraId); + zoom_factor = savedInstanceState.getInt("zoom_factor", 0); + if( MyDebug.LOG ) + Log.d(TAG, "found zoom_factor: " + zoom_factor); + focus_distance = savedInstanceState.getFloat("focus_distance", 0.0f); + if( MyDebug.LOG ) + Log.d(TAG, "found focus_distance: " + focus_distance); + } + + if( MyDebug.LOG ) + Log.d(TAG, "MyApplicationInterface: total time to create MyApplicationInterface: " + (System.currentTimeMillis() - debug_time)); + } + + void onSaveInstanceState(Bundle state) { + if( MyDebug.LOG ) + Log.d(TAG, "onSaveInstanceState"); + if( MyDebug.LOG ) + Log.d(TAG, "save cameraId: " + cameraId); + state.putInt("cameraId", cameraId); + if( MyDebug.LOG ) + Log.d(TAG, "save zoom_factor: " + zoom_factor); + state.putInt("zoom_factor", zoom_factor); + if( MyDebug.LOG ) + Log.d(TAG, "save focus_distance: " + focus_distance); + state.putFloat("focus_distance", focus_distance); + } + + void onDestroy() { + if( MyDebug.LOG ) + Log.d(TAG, "onDestroy"); + if( drawPreview != null ) { + drawPreview.onDestroy(); + } + if( imageSaver != null ) { + imageSaver.onDestroy(); + } + } + + LocationSupplier getLocationSupplier() { + return locationSupplier; + } + + StorageUtils getStorageUtils() { + return storageUtils; + } + + ImageSaver getImageSaver() { + return imageSaver; + } + + @Override + public Context getContext() { + return main_activity; + } + + @Override + public boolean useCamera2() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if( main_activity.supportsCamera2() ) { + return sharedPreferences.getBoolean(PreferenceKeys.getUseCamera2PreferenceKey(), false); + } + return false; + } + + @Override + public Location getLocation() { + return locationSupplier.getLocation(); + } + + @Override + public int createOutputVideoMethod() { + String action = main_activity.getIntent().getAction(); + if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "from video capture intent"); + Bundle myExtras = main_activity.getIntent().getExtras(); + if (myExtras != null) { + Uri intent_uri = myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if( intent_uri != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + intent_uri); + return VIDEOMETHOD_URI; + } + } + // if no EXTRA_OUTPUT, we should save to standard location, and will pass back the Uri of that location + if( MyDebug.LOG ) + Log.d(TAG, "intent uri not specified"); + // note that SAF URIs don't seem to work for calling applications (tested with Grabilla and "Photo Grabber Image From Video" (FreezeFrame)), so we use standard folder with non-SAF method + return VIDEOMETHOD_FILE; + } + boolean using_saf = storageUtils.isUsingSAF(); + return using_saf ? VIDEOMETHOD_SAF : VIDEOMETHOD_FILE; + } + + @Override + public File createOutputVideoFile() throws IOException { + last_video_file = storageUtils.createOutputMediaFile(StorageUtils.MEDIA_TYPE_VIDEO, "", "mp4", new Date()); + return last_video_file; + } + + @Override + public Uri createOutputVideoSAF() throws IOException { + last_video_file_saf = storageUtils.createOutputMediaFileSAF(StorageUtils.MEDIA_TYPE_VIDEO, "", "mp4", new Date()); + return last_video_file_saf; + } + + @Override + public Uri createOutputVideoUri() { + String action = main_activity.getIntent().getAction(); + if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "from video capture intent"); + Bundle myExtras = main_activity.getIntent().getExtras(); + if (myExtras != null) { + Uri intent_uri = myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if( intent_uri != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + intent_uri); + return intent_uri; + } + } + } + throw new RuntimeException(); // programming error if we arrived here + } + + @Override + public int getCameraIdPref() { + return cameraId; + } + + @Override + public String getFlashPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getFlashPreferenceKey(cameraId), ""); + } + + @Override + public String getFocusPref(boolean is_video) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getFocusPreferenceKey(cameraId, is_video), ""); + } + + @Override + public boolean isVideoPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getIsVideoPreferenceKey(), false); + } + + @Override + public String getSceneModePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getSceneModePreferenceKey(), "auto"); + } + + @Override + public String getColorEffectPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getColorEffectPreferenceKey(), "none"); + } + + @Override + public String getWhiteBalancePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getWhiteBalancePreferenceKey(), "auto"); + } + + @Override + public String getISOPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), "auto"); + } + + @Override + public int getExposureCompensationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String value = sharedPreferences.getString(PreferenceKeys.getExposurePreferenceKey(), "0"); + if( MyDebug.LOG ) + Log.d(TAG, "saved exposure value: " + value); + int exposure = 0; + try { + exposure = Integer.parseInt(value); + if( MyDebug.LOG ) + Log.d(TAG, "exposure: " + exposure); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "exposure invalid format, can't parse to int"); + } + return exposure; + } + + @Override + public Pair getCameraResolutionPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String resolution_value = sharedPreferences.getString(PreferenceKeys.getResolutionPreferenceKey(cameraId), ""); + if( MyDebug.LOG ) + Log.d(TAG, "resolution_value: " + resolution_value); + if( resolution_value.length() > 0 ) { + // parse the saved size, and make sure it is still valid + int index = resolution_value.indexOf(' '); + if( index == -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "resolution_value invalid format, can't find space"); + } + else { + String resolution_w_s = resolution_value.substring(0, index); + String resolution_h_s = resolution_value.substring(index+1); + if( MyDebug.LOG ) { + Log.d(TAG, "resolution_w_s: " + resolution_w_s); + Log.d(TAG, "resolution_h_s: " + resolution_h_s); + } + try { + int resolution_w = Integer.parseInt(resolution_w_s); + if( MyDebug.LOG ) + Log.d(TAG, "resolution_w: " + resolution_w); + int resolution_h = Integer.parseInt(resolution_h_s); + if( MyDebug.LOG ) + Log.d(TAG, "resolution_h: " + resolution_h); + return new Pair<>(resolution_w, resolution_h); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "resolution_value invalid format, can't parse w or h to int"); + } + } + } + return null; + } + + /** getImageQualityPref() returns the image quality used for the Camera Controller for taking a + * photo - in some cases, we may set that to a higher value, then perform processing on the + * resultant JPEG before resaving. This method returns the image quality setting to be used for + * saving the final image (as specified by the user). + */ + private int getSaveImageQualityPref() { + if( MyDebug.LOG ) + Log.d(TAG, "getSaveImageQualityPref"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String image_quality_s = sharedPreferences.getString(PreferenceKeys.getQualityPreferenceKey(), "90"); + int image_quality; + try { + image_quality = Integer.parseInt(image_quality_s); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.e(TAG, "image_quality_s invalid format: " + image_quality_s); + image_quality = 90; + } + return image_quality; + } + + @Override + public int getImageQualityPref(){ + if( MyDebug.LOG ) + Log.d(TAG, "getImageQualityPref"); + // see documentation for getSaveImageQualityPref(): in DRO mode we want to take the photo + // at 100% quality for post-processing, the final image will then be saved at the user requested + // setting + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.DRO ) + return 100; + return getSaveImageQualityPref(); + } + + @Override + public boolean getFaceDetectionPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getFaceDetectionPreferenceKey(), false); + } + + @Override + public String getVideoQualityPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getVideoQualityPreferenceKey(cameraId), ""); + } + + @Override + public boolean getVideoStabilizationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getVideoStabilizationPreferenceKey(), false); + } + + @Override + public boolean getForce4KPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if( cameraId == 0 && sharedPreferences.getBoolean(PreferenceKeys.getForceVideo4KPreferenceKey(), false) && main_activity.supportsForceVideo4K() ) { + return true; + } + return false; + } + + @Override + public String getVideoBitratePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getVideoBitratePreferenceKey(), "default"); + } + + @Override + public String getVideoFPSPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getVideoFPSPreferenceKey(), "default"); + } + + @Override + public long getVideoMaxDurationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String video_max_duration_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxDurationPreferenceKey(), "0"); + long video_max_duration; + try { + video_max_duration = (long)Integer.parseInt(video_max_duration_value) * 1000; + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_video_max_duration value: " + video_max_duration_value); + e.printStackTrace(); + video_max_duration = 0; + } + return video_max_duration; + } + + @Override + public int getVideoRestartTimesPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String restart_value = sharedPreferences.getString(PreferenceKeys.getVideoRestartPreferenceKey(), "0"); + int remaining_restart_video; + try { + remaining_restart_video = Integer.parseInt(restart_value); + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_video_restart value: " + restart_value); + e.printStackTrace(); + remaining_restart_video = 0; + } + return remaining_restart_video; + } + + + public int getVideoDelayTimesPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String restart_value = sharedPreferences.getString(PreferenceKeys.getVideoDelayPreferenceKey(), "0"); + int delay_time_video; + try { + delay_time_video = Integer.parseInt(restart_value); + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_video_restart value: " + restart_value); + e.printStackTrace(); + delay_time_video = 0; + } + return delay_time_video; + } + + long getVideoMaxFileSizeUserPref() { + if( MyDebug.LOG ) + Log.d(TAG, "getVideoMaxFileSizeUserPref"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String video_max_filesize_value = sharedPreferences.getString(PreferenceKeys.getVideoMaxFileSizePreferenceKey(), "0"); + long video_max_filesize; + try { + video_max_filesize = Integer.parseInt(video_max_filesize_value); + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_video_max_filesize value: " + video_max_filesize_value); + e.printStackTrace(); + video_max_filesize = 0; + } + if( MyDebug.LOG ) + Log.d(TAG, "video_max_filesize: " + video_max_filesize); + return video_max_filesize; + } + + private boolean getVideoRestartMaxFileSizeUserPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getVideoRestartMaxFileSizePreferenceKey(), true); + } + + @Override + public VideoMaxFileSize getVideoMaxFileSizePref() throws NoFreeStorageException { + if( MyDebug.LOG ) + Log.d(TAG, "getVideoMaxFileSizePref"); + VideoMaxFileSize video_max_filesize = new VideoMaxFileSize(); + video_max_filesize.max_filesize = getVideoMaxFileSizeUserPref(); + video_max_filesize.auto_restart = getVideoRestartMaxFileSizeUserPref(); + + /* Also if using internal memory without storage access framework, try to set the max filesize so we don't run out of space. + This is the only way to avoid the problem where videos become corrupt when run out of space - MediaRecorder doesn't stop on + its own, and no error is given! + If using SD card, it's not reliable to get the free storage (see https://sourceforge.net/p/opencamera/tickets/153/ ). + If using storage access framework, in theory we could check if this was on internal storage, but risk of getting it wrong... + so seems safest to leave (the main reason for using SAF is for SD cards, anyway). + */ + if( !storageUtils.isUsingSAF() ) { + String folder_name = storageUtils.getSaveLocation(); + if( MyDebug.LOG ) + Log.d(TAG, "saving to: " + folder_name); + boolean is_internal = false; + if( !folder_name.startsWith("/") ) { + is_internal = true; + } + else { + // if save folder path is a full path, see if it matches the "external" storage (which actually means "primary", which typically isn't an SD card these days) + File storage = Environment.getExternalStorageDirectory(); + if( MyDebug.LOG ) + Log.d(TAG, "compare to: " + storage.getAbsolutePath()); + if( folder_name.startsWith( storage.getAbsolutePath() ) ) + is_internal = true; + } + if( is_internal ) { + if( MyDebug.LOG ) + Log.d(TAG, "using internal storage"); + long free_memory = main_activity.freeMemory() * 1024 * 1024; + final long min_free_memory = 50000000; // how much free space to leave after video + // min_free_filesize is the minimum value to set for max file size: + // - no point trying to create a really short video + // - too short videos can end up being corrupted + // - also with auto-restart, if this is too small we'll end up repeatedly restarting and creating shorter and shorter videos + final long min_free_filesize = 20000000; + long available_memory = free_memory - min_free_memory; + if( test_set_available_memory ) { + available_memory = test_available_memory; + } + if( MyDebug.LOG ) { + Log.d(TAG, "free_memory: " + free_memory); + Log.d(TAG, "available_memory: " + available_memory); + } + if( available_memory > min_free_filesize ) { + if( video_max_filesize.max_filesize == 0 || video_max_filesize.max_filesize > available_memory ) { + video_max_filesize.max_filesize = available_memory; + // still leave auto_restart set to true - because even if we set a max filesize for running out of storage, the video may still hit a maximum limit before hand, if there's a device max limit set (typically ~2GB) + if( MyDebug.LOG ) + Log.d(TAG, "set video_max_filesize to avoid running out of space: " + video_max_filesize); + } + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "not enough free storage to record video"); + throw new NoFreeStorageException(); + } + } + } + + return video_max_filesize; + } + + @Override + public boolean getVideoFlashPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getVideoFlashPreferenceKey(), false); + } + + @Override + public boolean getVideoLowPowerCheckPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getVideoLowPowerCheckPreferenceKey(), true); + } + + @Override + public String getPreviewSizePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getPreviewSizePreferenceKey(), "preference_preview_size_wysiwyg"); + } + + @Override + public String getPreviewRotationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getRotatePreviewPreferenceKey(), "0"); + } + + @Override + public String getLockOrientationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getLockOrientationPreferenceKey(), "none"); + } + + @Override + public boolean getTouchCapturePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String value = sharedPreferences.getString(PreferenceKeys.getTouchCapturePreferenceKey(), "none"); + return value.equals("single"); + } + + @Override + public boolean getDoubleTapCapturePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String value = sharedPreferences.getString(PreferenceKeys.getTouchCapturePreferenceKey(), "none"); + return value.equals("double"); + } + + @Override + public boolean getPausePreviewPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getPausePreviewPreferenceKey(), false); + } + + @Override + public boolean getShowToastsPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getShowToastsPreferenceKey(), true); + } + + public boolean getThumbnailAnimationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getThumbnailAnimationPreferenceKey(), true); + } + + @Override + public boolean getShutterSoundPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getShutterSoundPreferenceKey(), true); + } + + @Override + public boolean getStartupFocusPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getStartupFocusPreferenceKey(), true); + } + + @Override + public long getTimerPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String timer_value = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + long timer_delay; + try { + timer_delay = (long)Integer.parseInt(timer_value) * 1000; + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_timer value: " + timer_value); + e.printStackTrace(); + timer_delay = 0; + } + return timer_delay; + } + + @Override + public String getRepeatPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getBurstModePreferenceKey(), "1"); + } + + @Override + public long getRepeatIntervalPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String timer_value = sharedPreferences.getString(PreferenceKeys.getBurstIntervalPreferenceKey(), "0"); + long timer_delay; + try { + timer_delay = (long)Integer.parseInt(timer_value) * 1000; + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse preference_burst_interval value: " + timer_value); + e.printStackTrace(); + timer_delay = 0; + } + return timer_delay; + } + + @Override + public boolean getGeotaggingPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getLocationPreferenceKey(), false); + } + + @Override + public boolean getRequireLocationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getRequireLocationPreferenceKey(), false); + } + + private boolean getGeodirectionPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getGPSDirectionPreferenceKey(), false); + } + + @Override + public boolean getRecordAudioPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getRecordAudioPreferenceKey(), true); + } + + @Override + public String getRecordAudioChannelsPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getRecordAudioChannelsPreferenceKey(), "audio_default"); + } + + @Override + public String getRecordAudioSourcePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getRecordAudioSourcePreferenceKey(), "audio_src_camcorder"); + } + + private boolean getAutoStabilisePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean auto_stabilise = sharedPreferences.getBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), false); + if( auto_stabilise && main_activity.supportsAutoStabilise() ) + return true; + return false; + } + + private String getStampPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getStampPreferenceKey(), "preference_stamp_no"); + } + + private String getStampDateFormatPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getStampDateFormatPreferenceKey(), "preference_stamp_dateformat_default"); + } + + private String getStampTimeFormatPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getStampTimeFormatPreferenceKey(), "preference_stamp_timeformat_default"); + } + + private String getStampGPSFormatPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getStampGPSFormatPreferenceKey(), "preference_stamp_gpsformat_default"); + } + + private String getTextStampPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getTextStampPreferenceKey(), ""); + } + + private int getTextStampFontSizePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + int font_size = 12; + String value = sharedPreferences.getString(PreferenceKeys.getStampFontSizePreferenceKey(), "12"); + if( MyDebug.LOG ) + Log.d(TAG, "saved font size: " + value); + try { + font_size = Integer.parseInt(value); + if( MyDebug.LOG ) + Log.d(TAG, "font_size: " + font_size); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "font size invalid format, can't parse to int"); + } + return font_size; + } + + private String getVideoSubtitlePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getVideoSubtitlePref(), "preference_video_subtitle_no"); + } + + @Override + public int getZoomPref() { + if( MyDebug.LOG ) + Log.d(TAG, "getZoomPref: " + zoom_factor); + return zoom_factor; + } + + @Override + public double getCalibratedLevelAngle() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getFloat(PreferenceKeys.getCalibratedLevelAnglePreferenceKey(), 0.0f); + } + + @Override + public long getExposureTimePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getLong(PreferenceKeys.getExposureTimePreferenceKey(), CameraController.EXPOSURE_TIME_DEFAULT); + } + + @Override + public float getFocusDistancePref() { + return focus_distance; + } + + @Override + public boolean isExpoBracketingPref() { + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.HDR || photo_mode == PhotoMode.ExpoBracketing ) + return true; + return false; + } + + @Override + public int getExpoBracketingNImagesPref() { + if( MyDebug.LOG ) + Log.d(TAG, "getExpoBracketingNImagesPref"); + int n_images; + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.HDR ) { + // always set 3 images for HDR + n_images = 3; + } + else { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String n_images_s = sharedPreferences.getString(PreferenceKeys.getExpoBracketingNImagesPreferenceKey(), "3"); + try { + n_images = Integer.parseInt(n_images_s); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.e(TAG, "n_images_s invalid format: " + n_images_s); + n_images = 3; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "n_images = " + n_images); + return n_images; + } + + @Override + public double getExpoBracketingStopsPref() { + if( MyDebug.LOG ) + Log.d(TAG, "getExpoBracketingStopsPref"); + double n_stops; + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.HDR ) { + // always set 2 stops for HDR + n_stops = 2.0; + } + else { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String n_stops_s = sharedPreferences.getString(PreferenceKeys.getExpoBracketingStopsPreferenceKey(), "2"); + try { + n_stops = Double.parseDouble(n_stops_s); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.e(TAG, "n_stops_s invalid format: " + n_stops_s); + n_stops = 2.0; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "n_stops = " + n_stops); + return n_stops; + } + + public PhotoMode getPhotoMode() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String photo_mode_pref = sharedPreferences.getString(PreferenceKeys.getPhotoModePreferenceKey(), "preference_photo_mode_std"); + boolean dro = photo_mode_pref.equals("preference_photo_mode_dro"); + if( dro && main_activity.supportsDRO() ) + return PhotoMode.DRO; + boolean hdr = photo_mode_pref.equals("preference_photo_mode_hdr"); + if( hdr && main_activity.supportsHDR() ) + return PhotoMode.HDR; + boolean expo_bracketing = photo_mode_pref.equals("preference_photo_mode_expo_bracketing"); + if( expo_bracketing && main_activity.supportsExpoBracketing() ) + return PhotoMode.ExpoBracketing; + return PhotoMode.Standard; + } + + @Override + public boolean getOptimiseAEForDROPref() { + PhotoMode photo_mode = getPhotoMode(); + return( photo_mode == PhotoMode.DRO ); + } + + @Override + public boolean isRawPref() { + if( isImageCaptureIntent() ) + return false; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getString(PreferenceKeys.getRawPreferenceKey(), "preference_raw_no").equals("preference_raw_yes"); + } + + @Override + public boolean useCamera2FakeFlash() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getCamera2FakeFlashPreferenceKey(), false); + } + + @Override + public boolean useCamera2FastBurst() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getCamera2FastBurstPreferenceKey(), true); + } + + @Override + public boolean isTestAlwaysFocus() { + if( MyDebug.LOG ) { + Log.d(TAG, "isTestAlwaysFocus: " + main_activity.is_test); + } + return main_activity.is_test; + } + + @Override + public void cameraSetup() { + main_activity.cameraSetup(); + drawPreview.clearContinuousFocusMove(); + } + + @Override + public void onContinuousFocusMove(boolean start) { + if( MyDebug.LOG ) + Log.d(TAG, "onContinuousFocusMove: " + start); + drawPreview.onContinuousFocusMove(start); + } + + @Override + public void touchEvent(MotionEvent event) { + main_activity.getMainUI().clearSeekBar(); + main_activity.getMainUI().closePopup(); + if( main_activity.usingKitKatImmersiveMode() ) { + main_activity.setImmersiveMode(false); + } + } + + @Override + public void startingVideo() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if( sharedPreferences.getBoolean(PreferenceKeys.getLockVideoPreferenceKey(), false) ) { + main_activity.lockScreen(); + } + main_activity.stopAudioListeners(); // important otherwise MediaRecorder will fail to start() if we have an audiolistener! Also don't want to have the speech recognizer going off + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + view.setImageResource(R.drawable.take_video_recording); + view.setContentDescription( getContext().getResources().getString(R.string.stop_video) ); + view.setTag(R.drawable.take_video_recording); // for testing + } + + @Override + public void startedVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "startedVideo()"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) { + if( !( main_activity.getMainUI().inImmersiveMode() && main_activity.usingKitKatImmersiveModeEverything() ) ) { + View pauseVideoButton = main_activity.findViewById(R.id.pause_video); + pauseVideoButton.setVisibility(View.VISIBLE); + } + main_activity.getMainUI().setPauseVideoContentDescription(); + } + final int video_method = this.createOutputVideoMethod(); + boolean dategeo_subtitles = getVideoSubtitlePref().equals("preference_video_subtitle_yes"); + if( dategeo_subtitles && video_method != ApplicationInterface.VIDEOMETHOD_URI ) { + final String preference_stamp_dateformat = this.getStampDateFormatPref(); + final String preference_stamp_timeformat = this.getStampTimeFormatPref(); + final String preference_stamp_gpsformat = this.getStampGPSFormatPref(); + final boolean store_location = getGeotaggingPref() && getLocation() != null; + final boolean store_geo_direction = main_activity.getPreview().hasGeoDirection() && getGeodirectionPref(); + class SubtitleVideoTimerTask extends TimerTask { + OutputStreamWriter writer; + private int count = 1; + + private String getSubtitleFilename(String video_filename) { + if( MyDebug.LOG ) + Log.d(TAG, "getSubtitleFilename"); + int indx = video_filename.indexOf('.'); + if( indx != -1 ) { + video_filename = video_filename.substring(0, indx); + } + video_filename = video_filename + ".srt"; + if( MyDebug.LOG ) + Log.d(TAG, "return filename: " + video_filename); + return video_filename; + } + + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "SubtitleVideoTimerTask run"); + long video_time = main_activity.getPreview().getVideoTime(); + if( !main_activity.getPreview().isVideoRecording() ) { + if( MyDebug.LOG ) + Log.d(TAG, "no longer video recording"); + return; + } + if( main_activity.getPreview().isVideoRecordingPaused() ) { + if( MyDebug.LOG ) + Log.d(TAG, "video recording is paused"); + return; + } + Date current_date = new Date(); + Calendar current_calendar = Calendar.getInstance(); + int offset_ms = current_calendar.get(Calendar.MILLISECOND); + if( MyDebug.LOG ) { + Log.d(TAG, "count: " + count); + Log.d(TAG, "offset_ms: " + offset_ms); + Log.d(TAG, "video_time: " + video_time); + } + String date_stamp = TextFormatter.getDateString(preference_stamp_dateformat, current_date); + String time_stamp = TextFormatter.getTimeString(preference_stamp_timeformat, current_date); + Location location = store_location ? getLocation() : null; + double geo_direction = store_geo_direction ? main_activity.getPreview().getGeoDirection() : 0.0; + String gps_stamp = main_activity.getTextFormatter().getGPSString(preference_stamp_gpsformat, store_location, location, store_geo_direction, geo_direction); + if( MyDebug.LOG ) { + Log.d(TAG, "date_stamp: " + date_stamp); + Log.d(TAG, "time_stamp: " + time_stamp); + Log.d(TAG, "gps_stamp: " + gps_stamp); + } + String datetime_stamp = ""; + if( date_stamp.length() > 0 ) + datetime_stamp += date_stamp; + if( time_stamp.length() > 0 ) { + if( datetime_stamp.length() > 0 ) + datetime_stamp += " "; + datetime_stamp += time_stamp; + } + String subtitles = ""; + if( datetime_stamp.length() > 0 ) + subtitles += datetime_stamp + "\n"; + if( gps_stamp.length() > 0 ) + subtitles += gps_stamp + "\n"; + if( subtitles.length() == 0 ) { + return; + } + long video_time_from = video_time - offset_ms; + long video_time_to = video_time_from + 999; + if( video_time_from < 0 ) + video_time_from = 0; + String subtitle_time_from = TextFormatter.formatTimeMS(video_time_from); + String subtitle_time_to = TextFormatter.formatTimeMS(video_time_to); + try { + synchronized( this ) { + if( writer == null ) { + if( video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + String subtitle_filename = last_video_file.getAbsolutePath(); + subtitle_filename = getSubtitleFilename(subtitle_filename); + writer = new FileWriter(subtitle_filename); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "last_video_file_saf: " + last_video_file_saf); + File file = storageUtils.getFileFromDocumentUriSAF(last_video_file_saf, false); + String subtitle_filename = file.getName(); + subtitle_filename = getSubtitleFilename(subtitle_filename); + Uri subtitle_uri = storageUtils.createOutputFileSAF(subtitle_filename, ""); // don't set a mimetype, as we don't want it to append a new extension + ParcelFileDescriptor pfd_saf = getContext().getContentResolver().openFileDescriptor(subtitle_uri, "w"); + writer = new FileWriter(pfd_saf.getFileDescriptor()); + } + } + if( writer != null ) { + writer.append(Integer.toString(count)); + writer.append('\n'); + writer.append(subtitle_time_from); + writer.append(" --> "); + writer.append(subtitle_time_to); + writer.append('\n'); + writer.append(subtitles); // subtitles should include the '\n' at the end + writer.append('\n'); // additional newline to indicate end of this subtitle + writer.flush(); + // n.b., we flush rather than closing/reopening the writer each time, as appending doesn't seem to work with storage access framework + } + } + count++; + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "SubtitleVideoTimerTask failed to create or write"); + e.printStackTrace(); + } + if( MyDebug.LOG ) + Log.d(TAG, "SubtitleVideoTimerTask exit"); + } + + public boolean cancel() { + if( MyDebug.LOG ) + Log.d(TAG, "SubtitleVideoTimerTask cancel"); + synchronized( this ) { + if( writer != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "close writer"); + try { + writer.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + writer = null; + } + } + return super.cancel(); + } + } + subtitleVideoTimer.schedule(subtitleVideoTimerTask = new SubtitleVideoTimerTask(), 0, 1000); + } + } + + @Override + public void stoppingVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "stoppingVideo()"); + main_activity.unlockScreen(); + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + view.setImageResource(R.drawable.take_video_selector); + view.setContentDescription( getContext().getResources().getString(R.string.start_video) ); + view.setTag(R.drawable.take_video_selector); // for testing + } + + @Override + public void stoppedVideo(final int video_method, final Uri uri, final String filename) { + if( MyDebug.LOG ) { + Log.d(TAG, "stoppedVideo"); + Log.d(TAG, "video_method " + video_method); + Log.d(TAG, "uri " + uri); + Log.d(TAG, "filename " + filename); + } + View pauseVideoButton = main_activity.findViewById(R.id.pause_video); + pauseVideoButton.setVisibility(View.INVISIBLE); + main_activity.getMainUI().setPauseVideoContentDescription(); // just to be safe + if( subtitleVideoTimerTask != null ) { + subtitleVideoTimerTask.cancel(); + subtitleVideoTimerTask = null; + } + + boolean done = false; + if( video_method == VIDEOMETHOD_FILE ) { + if( filename != null ) { + File file = new File(filename); + storageUtils.broadcastFile(file, false, true, true); + done = true; + } + } + else { + if( uri != null ) { + // see note in onPictureTaken() for where we call broadcastFile for SAF photos + File real_file = storageUtils.getFileFromDocumentUriSAF(uri, false); + if( MyDebug.LOG ) + Log.d(TAG, "real_file: " + real_file); + if( real_file != null ) { + storageUtils.broadcastFile(real_file, false, true, true); + main_activity.test_last_saved_image = real_file.getAbsolutePath(); + } + else { + // announce the SAF Uri + storageUtils.announceUri(uri, false, true); + } + done = true; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "done? " + done); + + String action = main_activity.getIntent().getAction(); + if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( done && video_method == VIDEOMETHOD_FILE ) { + // do nothing here - we end the activity from storageUtils.broadcastFile after the file has been scanned, as it seems caller apps seem to prefer the content:// Uri rather than one based on a File + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "from video capture intent"); + Intent output = null; + if( done ) { + // may need to pass back the Uri we saved to, if the calling application didn't specify a Uri + // set note above for VIDEOMETHOD_FILE + // n.b., currently this code is not used, as we always switch to VIDEOMETHOD_FILE if the calling application didn't specify a Uri, but I've left this here for possible future behaviour + if( video_method == VIDEOMETHOD_SAF ) { + output = new Intent(); + output.setData(uri); + if( MyDebug.LOG ) + Log.d(TAG, "pass back output uri [saf]: " + output.getData()); + } + } + main_activity.setResult(done ? Activity.RESULT_OK : Activity.RESULT_CANCELED, output); + main_activity.finish(); + } + } + else if( done ) { + // create thumbnail + long debug_time = System.currentTimeMillis(); + Bitmap thumbnail = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + if( video_method == VIDEOMETHOD_FILE ) { + File file = new File(filename); + retriever.setDataSource(file.getPath()); + } + else { + ParcelFileDescriptor pfd_saf = getContext().getContentResolver().openFileDescriptor(uri, "r"); + retriever.setDataSource(pfd_saf.getFileDescriptor()); + } + thumbnail = retriever.getFrameAtTime(-1); + } + catch(FileNotFoundException | /*IllegalArgumentException |*/ RuntimeException e) { + // video file wasn't saved or corrupt video file? + Log.d(TAG, "failed to find thumbnail"); + e.printStackTrace(); + } + finally { + try { + retriever.release(); + } + catch(RuntimeException ex) { + // ignore + } + } + if( thumbnail != null ) { + ImageButton galleryButton = (ImageButton) main_activity.findViewById(R.id.gallery); + int width = thumbnail.getWidth(); + int height = thumbnail.getHeight(); + if( MyDebug.LOG ) + Log.d(TAG, " video thumbnail size " + width + " x " + height); + if( width > galleryButton.getWidth() ) { + float scale = (float) galleryButton.getWidth() / width; + int new_width = Math.round(scale * width); + int new_height = Math.round(scale * height); + if( MyDebug.LOG ) + Log.d(TAG, " scale video thumbnail to " + new_width + " x " + new_height); + Bitmap scaled_thumbnail = Bitmap.createScaledBitmap(thumbnail, new_width, new_height, true); + // careful, as scaled_thumbnail is sometimes not a copy! + if( scaled_thumbnail != thumbnail ) { + thumbnail.recycle(); + thumbnail = scaled_thumbnail; + } + } + final Bitmap thumbnail_f = thumbnail; + main_activity.runOnUiThread(new Runnable() { + public void run() { + updateThumbnail(thumbnail_f); + } + }); + } + if( MyDebug.LOG ) + Log.d(TAG, " time to create thumbnail: " + (System.currentTimeMillis() - debug_time)); + } + } + + @Override + public void onVideoInfo(int what, int extra) { + // we don't show a toast for MEDIA_RECORDER_INFO_MAX_DURATION_REACHED - conflicts with "n repeats to go" toast from Preview + if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED ) { + if( MyDebug.LOG ) + Log.d(TAG, "max filesize reached"); + int message_id = R.string.video_max_filesize; + main_activity.getPreview().showToast(null, message_id); + // in versions 1.24 and 1.24, there was a bug where we had "info_" for onVideoError and "error_" for onVideoInfo! + // fixed in 1.25; also was correct for 1.23 and earlier + String debug_value = "info_" + what + "_" + extra; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("last_video_error", debug_value); + editor.apply(); + } + } + + @Override + public void onFailedStartPreview() { + main_activity.getPreview().showToast(null, R.string.failed_to_start_camera_preview); + } + + @Override + public void onCameraError() { + main_activity.getPreview().showToast(null, R.string.camera_error); + } + + @Override + public void onPhotoError() { + main_activity.getPreview().showToast(null, R.string.failed_to_take_picture); + } + + @Override + public void onVideoError(int what, int extra) { + if( MyDebug.LOG ) { + Log.d(TAG, "onVideoError: " + what + " extra: " + extra); + } + int message_id = R.string.video_error_unknown; + if( what == MediaRecorder.MEDIA_ERROR_SERVER_DIED ) { + if( MyDebug.LOG ) + Log.d(TAG, "error: server died"); + message_id = R.string.video_error_server_died; + } + main_activity.getPreview().showToast(null, message_id); + // in versions 1.24 and 1.24, there was a bug where we had "info_" for onVideoError and "error_" for onVideoInfo! + // fixed in 1.25; also was correct for 1.23 and earlier + String debug_value = "error_" + what + "_" + extra; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("last_video_error", debug_value); + editor.apply(); + } + + @Override + public void onVideoRecordStartError(CamcorderProfile profile) { + if( MyDebug.LOG ) + Log.d(TAG, "onVideoRecordStartError"); + String error_message; + String features = main_activity.getPreview().getErrorFeatures(profile); + if( features.length() > 0 ) { + error_message = getContext().getResources().getString(R.string.sorry) + ", " + features + " " + getContext().getResources().getString(R.string.not_supported); + } + else { + error_message = getContext().getResources().getString(R.string.failed_to_record_video); + } + main_activity.getPreview().showToast(null, error_message); + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + view.setImageResource(R.drawable.take_video_selector); + view.setContentDescription( getContext().getResources().getString(R.string.start_video) ); + view.setTag(R.drawable.take_video_selector); // for testing + } + + @Override + public void onVideoRecordStopError(CamcorderProfile profile) { + if( MyDebug.LOG ) + Log.d(TAG, "onVideoRecordStopError"); + //main_activity.getPreview().showToast(null, R.string.failed_to_record_video); + String features = main_activity.getPreview().getErrorFeatures(profile); + String error_message = getContext().getResources().getString(R.string.video_may_be_corrupted); + if( features.length() > 0 ) { + error_message += ", " + features + " " + getContext().getResources().getString(R.string.not_supported); + } + main_activity.getPreview().showToast(null, error_message); + } + + @Override + public void onFailedReconnectError() { + main_activity.getPreview().showToast(null, R.string.failed_to_reconnect_camera); + } + + @Override + public void onFailedCreateVideoFileError() { + main_activity.getPreview().showToast(null, R.string.failed_to_save_video); + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + view.setImageResource(R.drawable.take_video_selector); + view.setContentDescription( getContext().getResources().getString(R.string.start_video) ); + view.setTag(R.drawable.take_video_selector); // for testing + } + + @Override + public void hasPausedPreview(boolean paused) { + View shareButton = main_activity.findViewById(R.id.share); + View trashButton = main_activity.findViewById(R.id.trash); + if( paused ) { + shareButton.setVisibility(View.VISIBLE); + trashButton.setVisibility(View.VISIBLE); + } + else { + shareButton.setVisibility(View.GONE); + trashButton.setVisibility(View.GONE); + this.clearLastImages(); + } + } + + @Override + public void cameraInOperation(boolean in_operation) { + if( MyDebug.LOG ) + Log.d(TAG, "cameraInOperation: " + in_operation); + if( !in_operation && used_front_screen_flash ) { + main_activity.setBrightnessForCamera(false); // ensure screen brightness matches user preference, after using front screen flash + used_front_screen_flash = false; + } + drawPreview.cameraInOperation(in_operation); + main_activity.getMainUI().showGUI(!in_operation); + } + + @Override + public void turnFrontScreenFlashOn() { + if( MyDebug.LOG ) + Log.d(TAG, "turnFrontScreenFlashOn"); + used_front_screen_flash = true; + main_activity.setBrightnessForCamera(true); // ensure we have max screen brightness, even if user preference not set for max brightness + drawPreview.turnFrontScreenFlashOn(); + } + + @Override + public void onCaptureStarted() { + if( MyDebug.LOG ) + Log.d(TAG, "onCaptureStarted"); + drawPreview.onCaptureStarted(); + } + + @Override + public void onPictureCompleted() { + if( MyDebug.LOG ) + Log.d(TAG, "onPictureCompleted"); + // call this, so that if pause-preview-after-taking-photo option is set, we remove the "taking photo" border indicator straight away + drawPreview.cameraInOperation(false); + } + + @Override + public void cameraClosed() { + main_activity.getMainUI().clearSeekBar(); + main_activity.getMainUI().destroyPopup(); // need to close popup - and when camera reopened, it may have different settings + drawPreview.clearContinuousFocusMove(); + } + + void updateThumbnail(Bitmap thumbnail) { + if( MyDebug.LOG ) + Log.d(TAG, "updateThumbnail"); + main_activity.updateGalleryIcon(thumbnail); + drawPreview.updateThumbnail(thumbnail); + if( this.getPausePreviewPref() ) { + drawPreview.showLastImage(); + } + } + + @Override + public void timerBeep(long remaining_time) { + if( MyDebug.LOG ) { + Log.d(TAG, "timerBeep()"); + Log.d(TAG, "remaining_time: " + remaining_time); + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if( sharedPreferences.getBoolean(PreferenceKeys.getTimerBeepPreferenceKey(), true) ) { + if( MyDebug.LOG ) + Log.d(TAG, "play beep!"); + boolean is_last = remaining_time <= 1000; + main_activity.playSound(is_last ? R.raw.beep_hi : R.raw.beep); + } + if( sharedPreferences.getBoolean(PreferenceKeys.getTimerSpeakPreferenceKey(), false) ) { + if( MyDebug.LOG ) + Log.d(TAG, "speak countdown!"); + int remaining_time_s = (int)(remaining_time/1000); + if( remaining_time_s <= 60 ) + main_activity.speak("" + remaining_time_s); + } + } + + @Override + public void layoutUI() { + main_activity.getMainUI().layoutUI(); + } + + @Override + public void multitouchZoom(int new_zoom) { + main_activity.getMainUI().setSeekbarZoom(); + } + + @Override + public void setCameraIdPref(int cameraId) { + this.cameraId = cameraId; + } + + @Override + public void setFlashPref(String flash_value) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getFlashPreferenceKey(cameraId), flash_value); + editor.apply(); + } + + @Override + public void setFocusPref(String focus_value, boolean is_video) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getFocusPreferenceKey(cameraId, is_video), focus_value); + editor.apply(); + // focus may be updated by preview (e.g., when switching to/from video mode) + final int visibility = main_activity.getPreview().getCurrentFocusValue() != null && main_activity.getPreview().getCurrentFocusValue().equals("focus_mode_manual2") ? View.VISIBLE : View.INVISIBLE; + View focusSeekBar = main_activity.findViewById(R.id.focus_seekbar); + focusSeekBar.setVisibility(visibility); + } + + @Override + public void setVideoPref(boolean is_video) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getIsVideoPreferenceKey(), is_video); + editor.apply(); + } + + @Override + public void setSceneModePref(String scene_mode) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getSceneModePreferenceKey(), scene_mode); + editor.apply(); + } + + @Override + public void clearSceneModePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getSceneModePreferenceKey()); + editor.apply(); + } + + @Override + public void setColorEffectPref(String color_effect) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getColorEffectPreferenceKey(), color_effect); + editor.apply(); + } + + @Override + public void clearColorEffectPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getColorEffectPreferenceKey()); + editor.apply(); + } + + @Override + public void setWhiteBalancePref(String white_balance) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getWhiteBalancePreferenceKey(), white_balance); + editor.apply(); + } + + @Override + public void clearWhiteBalancePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getWhiteBalancePreferenceKey()); + editor.apply(); + } + + @Override + public void setISOPref(String iso) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getISOPreferenceKey(), iso); + editor.apply(); + } + + @Override + public void clearISOPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getISOPreferenceKey()); + editor.apply(); + } + + @Override + public void setExposureCompensationPref(int exposure) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getExposurePreferenceKey(), "" + exposure); + editor.apply(); + } + + @Override + public void clearExposureCompensationPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getExposurePreferenceKey()); + editor.apply(); + } + + @Override + public void setCameraResolutionPref(int width, int height) { + String resolution_value = width + " " + height; + if( MyDebug.LOG ) { + Log.d(TAG, "save new resolution_value: " + resolution_value); + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(cameraId), resolution_value); + editor.apply(); + } + + @Override + public void setVideoQualityPref(String video_quality) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getVideoQualityPreferenceKey(cameraId), video_quality); + editor.apply(); + } + + @Override + public void setZoomPref(int zoom) { + if( MyDebug.LOG ) + Log.d(TAG, "setZoomPref: " + zoom); + this.zoom_factor = zoom; + } + + @Override + public void requestCameraPermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestCameraPermission"); + main_activity.requestCameraPermission(); + } + + @Override + public void requestStoragePermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestStoragePermission"); + main_activity.requestStoragePermission(); + } + + @Override + public void requestRecordAudioPermission() { + if( MyDebug.LOG ) + Log.d(TAG, "requestRecordAudioPermission"); + main_activity.requestRecordAudioPermission(); + } + + @Override + public void setExposureTimePref(long exposure_time) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putLong(PreferenceKeys.getExposureTimePreferenceKey(), exposure_time); + editor.apply(); + } + + @Override + public void clearExposureTimePref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(PreferenceKeys.getExposureTimePreferenceKey()); + editor.apply(); + } + + @Override + public void setFocusDistancePref(float focus_distance) { + this.focus_distance = focus_distance; + } + + private int getStampFontColor() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + String color = sharedPreferences.getString(PreferenceKeys.getStampFontColorPreferenceKey(), "#ffffff"); + return Color.parseColor(color); + } + + @Override + public void onDrawPreview(Canvas canvas) { + drawPreview.onDrawPreview(canvas); + } + + public enum Alignment { + ALIGNMENT_TOP, + ALIGNMENT_CENTRE, + ALIGNMENT_BOTTOM + } + + public void drawTextWithBackground(Canvas canvas, Paint paint, String text, int foreground, int background, int location_x, int location_y) { + drawTextWithBackground(canvas, paint, text, foreground, background, location_x, location_y, Alignment.ALIGNMENT_BOTTOM); + } + + public void drawTextWithBackground(Canvas canvas, Paint paint, String text, int foreground, int background, int location_x, int location_y, Alignment alignment_y) { + drawTextWithBackground(canvas, paint, text, foreground, background, location_x, location_y, alignment_y, null, true); + } + + public void drawTextWithBackground(Canvas canvas, Paint paint, String text, int foreground, int background, int location_x, int location_y, Alignment alignment_y, String ybounds_text, boolean shadow) { + final float scale = getContext().getResources().getDisplayMetrics().density; + paint.setStyle(Paint.Style.FILL); + paint.setColor(background); + paint.setAlpha(64); + int alt_height = 0; + if( ybounds_text != null ) { + paint.getTextBounds(ybounds_text, 0, ybounds_text.length(), text_bounds); + alt_height = text_bounds.bottom - text_bounds.top; + } + paint.getTextBounds(text, 0, text.length(), text_bounds); + if( ybounds_text != null ) { + text_bounds.bottom = text_bounds.top + alt_height; + } + final int padding = (int) (2 * scale + 0.5f); // convert dps to pixels + if( paint.getTextAlign() == Paint.Align.RIGHT || paint.getTextAlign() == Paint.Align.CENTER ) { + float width = paint.measureText(text); // n.b., need to use measureText rather than getTextBounds here + /*if( MyDebug.LOG ) + Log.d(TAG, "width: " + width);*/ + if( paint.getTextAlign() == Paint.Align.CENTER ) + width /= 2.0f; + text_bounds.left -= width; + text_bounds.right -= width; + } + /*if( MyDebug.LOG ) + Log.d(TAG, "text_bounds left-right: " + text_bounds.left + " , " + text_bounds.right);*/ + text_bounds.left += location_x - padding; + text_bounds.right += location_x + padding; + // unclear why we need the offset of -1, but need this to align properly on Galaxy Nexus at least + int top_y_diff = - text_bounds.top + padding - 1; + if( alignment_y == Alignment.ALIGNMENT_TOP ) { + int height = text_bounds.bottom - text_bounds.top + 2*padding; + text_bounds.top = location_y - 1; + text_bounds.bottom = text_bounds.top + height; + location_y += top_y_diff; + } + else if( alignment_y == Alignment.ALIGNMENT_CENTRE ) { + int height = text_bounds.bottom - text_bounds.top + 2*padding; + int y_diff = - text_bounds.top + padding - 1; + text_bounds.top = (int)(0.5 * ( (location_y - 1) + (text_bounds.top + location_y - padding) )); // average of ALIGNMENT_TOP and ALIGNMENT_BOTTOM + text_bounds.bottom = text_bounds.top + height; + location_y += (int)(0.5*top_y_diff); // average of ALIGNMENT_TOP and ALIGNMENT_BOTTOM + } + else { + text_bounds.top += location_y - padding; + text_bounds.bottom += location_y + padding; + } + if( shadow ) { + canvas.drawRect(text_bounds, paint); + } + paint.setColor(foreground); + canvas.drawText(text, location_x, location_y, paint); + } + + private boolean saveInBackground(boolean image_capture_intent) { + boolean do_in_background = true; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if( !sharedPreferences.getBoolean(PreferenceKeys.getBackgroundPhotoSavingPreferenceKey(), true) ) + do_in_background = false; + else if( image_capture_intent ) + do_in_background = false; + else if( getPausePreviewPref() ) + do_in_background = false; + return do_in_background; + } + + private boolean isImageCaptureIntent() { + boolean image_capture_intent = false; + String action = main_activity.getIntent().getAction(); + if( MediaStore.ACTION_IMAGE_CAPTURE.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "from image capture intent"); + image_capture_intent = true; + } + return image_capture_intent; + } + + private boolean saveImage(boolean is_hdr, boolean save_expo, List images, Date current_date) { + if( MyDebug.LOG ) + Log.d(TAG, "saveImage"); + + System.gc(); + + boolean image_capture_intent = isImageCaptureIntent(); + Uri image_capture_intent_uri = null; + if( image_capture_intent ) { + if( MyDebug.LOG ) + Log.d(TAG, "from image capture intent"); + Bundle myExtras = main_activity.getIntent().getExtras(); + if (myExtras != null) { + image_capture_intent_uri = myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + image_capture_intent_uri); + } + } + + boolean using_camera2 = main_activity.getPreview().usingCamera2API(); + int image_quality = getSaveImageQualityPref(); + if( MyDebug.LOG ) + Log.d(TAG, "image_quality: " + image_quality); + boolean do_auto_stabilise = getAutoStabilisePref() && main_activity.getPreview().hasLevelAngle(); + double level_angle = do_auto_stabilise ? main_activity.getPreview().getLevelAngle() : 0.0; + if( do_auto_stabilise && main_activity.test_have_angle ) + level_angle = main_activity.test_angle; + if( do_auto_stabilise && main_activity.test_low_memory ) + level_angle = 45.0; + // I have received crashes where camera_controller was null - could perhaps happen if this thread was running just as the camera is closing? + boolean is_front_facing = main_activity.getPreview().getCameraController() != null && main_activity.getPreview().getCameraController().isFrontFacing(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean mirror = is_front_facing && sharedPreferences.getString(PreferenceKeys.getFrontCameraMirrorKey(), "preference_front_camera_mirror_no").equals("preference_front_camera_mirror_photo"); + String preference_stamp = this.getStampPref(); + String preference_textstamp = this.getTextStampPref(); + int font_size = getTextStampFontSizePref(); + int color = getStampFontColor(); + String pref_style = sharedPreferences.getString(PreferenceKeys.getStampStyleKey(), "preference_stamp_style_shadowed"); + String preference_stamp_dateformat = this.getStampDateFormatPref(); + String preference_stamp_timeformat = this.getStampTimeFormatPref(); + String preference_stamp_gpsformat = this.getStampGPSFormatPref(); + boolean store_location = getGeotaggingPref() && getLocation() != null; + Location location = store_location ? getLocation() : null; + boolean store_geo_direction = main_activity.getPreview().hasGeoDirection() && getGeodirectionPref(); + double geo_direction = store_geo_direction ? main_activity.getPreview().getGeoDirection() : 0.0; + boolean has_thumbnail_animation = getThumbnailAnimationPref(); + + boolean do_in_background = saveInBackground(image_capture_intent); + + int sample_factor = 1; + if( !this.getPausePreviewPref() ) { + // if pausing the preview, we use the thumbnail also for the preview, so don't downsample + // otherwise, we can downsample by 4 to increase performance, without noticeable loss in visual quality (even for the thumbnail animation) + sample_factor *= 4; + if( !has_thumbnail_animation ) { + // can use even lower resolution if we don't have the thumbnail animation + sample_factor *= 4; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "sample_factor: " + sample_factor); + + boolean success = imageSaver.saveImageJpeg(do_in_background, is_hdr, save_expo, images, + image_capture_intent, image_capture_intent_uri, + using_camera2, image_quality, + do_auto_stabilise, level_angle, + is_front_facing, + mirror, + current_date, + preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, + store_location, location, store_geo_direction, geo_direction, + sample_factor); + + if( MyDebug.LOG ) + Log.d(TAG, "saveImage complete, success: " + success); + + return success; + } + + @Override + public boolean onPictureTaken(byte [] data, Date current_date) { + if( MyDebug.LOG ) + Log.d(TAG, "onPictureTaken"); + + List images = new ArrayList<>(); + images.add(data); + + boolean is_hdr = false; + // note, multi-image HDR and expo is handled under onBurstPictureTaken; here we look for DRO, as that's the photo mode to set + // single image HDR + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.DRO ) { + is_hdr = true; + } + boolean success = saveImage(is_hdr, false, images, current_date); + + if( MyDebug.LOG ) + Log.d(TAG, "onPictureTaken complete, success: " + success); + + return success; + } + + @Override + public boolean onBurstPictureTaken(List images, Date current_date) { + if( MyDebug.LOG ) + Log.d(TAG, "onBurstPictureTaken: received " + images.size() + " images"); + + boolean success; + PhotoMode photo_mode = getPhotoMode(); + if( photo_mode == PhotoMode.HDR ) { + if( MyDebug.LOG ) + Log.d(TAG, "HDR mode"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean save_expo = sharedPreferences.getBoolean(PreferenceKeys.getHDRSaveExpoPreferenceKey(), false); + if( MyDebug.LOG ) + Log.d(TAG, "save_expo: " + save_expo); + + success = saveImage(true, save_expo, images, current_date); + } + else { + if( MyDebug.LOG ) { + Log.d(TAG, "exposure bracketing mode mode"); + if( photo_mode != PhotoMode.ExpoBracketing ) + Log.e(TAG, "onBurstPictureTaken called with unexpected photo mode?!: " + photo_mode); + } + + success = saveImage(false, true, images, current_date); + } + return success; + } + + @Override + public boolean onRawPictureTaken(DngCreator dngCreator, Image image, Date current_date) { + if( MyDebug.LOG ) + Log.d(TAG, "onRawPictureTaken"); + System.gc(); + + boolean do_in_background = saveInBackground(false); + + boolean success = imageSaver.saveImageRaw(do_in_background, dngCreator, image, current_date); + + if( MyDebug.LOG ) + Log.d(TAG, "onRawPictureTaken complete"); + return success; + } + + void addLastImage(File file, boolean share) { + if( MyDebug.LOG ) { + Log.d(TAG, "addLastImage: " + file); + Log.d(TAG, "share?: " + share); + } + last_images_saf = false; + LastImage last_image = new LastImage(file.getAbsolutePath(), share); + last_images.add(last_image); + } + + void addLastImageSAF(Uri uri, boolean share) { + if( MyDebug.LOG ) { + Log.d(TAG, "addLastImageSAF: " + uri); + Log.d(TAG, "share?: " + share); + } + last_images_saf = true; + LastImage last_image = new LastImage(uri, share); + last_images.add(last_image); + } + + void clearLastImages() { + if( MyDebug.LOG ) + Log.d(TAG, "clearLastImages"); + last_images_saf = false; + last_images.clear(); + drawPreview.clearLastImage(); + } + + void shareLastImage() { + if( MyDebug.LOG ) + Log.d(TAG, "shareLastImage"); + Preview preview = main_activity.getPreview(); + if( preview.isPreviewPaused() ) { + LastImage share_image = null; + for(int i=0;i"; + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + for(int i=start;i 0 ) { + about_string.append("\nLast video error: "); + about_string.append(last_video_error); + } + } + if( preview_widths != null && preview_heights != null ) { + about_string.append("\nPreview resolutions: "); + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(preview_widths[i]); + about_string.append("x"); + about_string.append(preview_heights[i]); + } + } + about_string.append("\nPreview resolution: "); + about_string.append(preview_width); + about_string.append("x"); + about_string.append(preview_height); + if( widths != null && heights != null ) { + about_string.append("\nPhoto resolutions: "); + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(widths[i]); + about_string.append("x"); + about_string.append(heights[i]); + } + } + about_string.append("\nPhoto resolution: "); + about_string.append(resolution_width); + about_string.append("x"); + about_string.append(resolution_height); + if( video_quality != null ) { + about_string.append("\nVideo qualities: "); + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(video_quality[i]); + } + } + if( video_widths != null && video_heights != null ) { + about_string.append("\nVideo resolutions: "); + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(video_widths[i]); + about_string.append("x"); + about_string.append(video_heights[i]); + } + } + about_string.append("\nVideo quality: "); + about_string.append(current_video_quality); + about_string.append("\nVideo frame width: "); + about_string.append(video_frame_width); + about_string.append("\nVideo frame height: "); + about_string.append(video_frame_height); + about_string.append("\nVideo bit rate: "); + about_string.append(video_bit_rate); + about_string.append("\nVideo frame rate: "); + about_string.append(video_frame_rate); + about_string.append("\nAuto-stabilise?: "); + about_string.append(getString(supports_auto_stabilise ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nAuto-stabilise enabled?: "); + about_string.append(sharedPreferences.getBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), false)); + about_string.append("\nFace detection?: "); + about_string.append(getString(supports_face_detection ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nRAW?: "); + about_string.append(getString(supports_raw ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nVideo stabilization?: "); + about_string.append(getString(supports_video_stabilization ? R.string.about_available : R.string.about_not_available)); + about_string.append("\nCan disable shutter sound?: "); + about_string.append(getString(can_disable_shutter_sound ? R.string.answer_yes : R.string.answer_no)); + about_string.append("\nFlash modes: "); + String [] flash_values = bundle.getStringArray("flash_values"); + if( flash_values != null && flash_values.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(flash_values[i]); + } + } + else { + about_string.append("None"); + } + about_string.append("\nFocus modes: "); + String [] focus_values = bundle.getStringArray("focus_values"); + if( focus_values != null && focus_values.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(focus_values[i]); + } + } + else { + about_string.append("None"); + } + about_string.append("\nColor effects: "); + String [] color_effects_values = bundle.getStringArray("color_effects"); + if( color_effects_values != null && color_effects_values.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(color_effects_values[i]); + } + } + else { + about_string.append("None"); + } + about_string.append("\nScene modes: "); + String [] scene_modes_values = bundle.getStringArray("scene_modes"); + if( scene_modes_values != null && scene_modes_values.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(scene_modes_values[i]); + } + } + else { + about_string.append("None"); + } + about_string.append("\nWhite balances: "); + String [] white_balances_values = bundle.getStringArray("white_balances"); + if( white_balances_values != null && white_balances_values.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(white_balances_values[i]); + } + } + else { + about_string.append("None"); + } + about_string.append("\nISOs: "); + String [] isos = bundle.getStringArray("isos"); + if( isos != null && isos.length > 0 ) { + for(int i=0;i 0 ) { + about_string.append(", "); + } + about_string.append(isos[i]); + } + } + else { + about_string.append("None"); + } + String iso_key = bundle.getString("iso_key"); + if( iso_key != null ) { + about_string.append("\nISO key: "); + about_string.append(iso_key); + } + + about_string.append("\nUsing SAF?: "); + about_string.append(sharedPreferences.getBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false)); + String save_location = sharedPreferences.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + about_string.append("\nSave Location: "); + about_string.append(save_location); + String save_location_saf = sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + about_string.append("\nSave Location SAF: "); + about_string.append(save_location_saf); + + about_string.append("\nParameters: "); + String parameters_string = bundle.getString("parameters_string"); + if( parameters_string != null ) { + about_string.append(parameters_string); + } + else { + about_string.append("None"); + } + + alertDialog.setMessage(about_string); + alertDialog.setPositiveButton(R.string.about_ok, null); + alertDialog.setNegativeButton(R.string.about_copy_to_clipboard, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + if( MyDebug.LOG ) + Log.d(TAG, "user clicked copy to clipboard"); + ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Activity.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("OpenCamera About", about_string); + clipboard.setPrimaryClip(clip); + } + }); + alertDialog.show(); + return false; + } + return false; + } + }); + } + + { + final Preference pref = findPreference("preference_reset"); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference arg0) { + if( pref.getKey().equals("preference_reset") ) { + if( MyDebug.LOG ) + Log.d(TAG, "user clicked reset"); + new AlertDialog.Builder(MyPreferenceFragment.this.getActivity()) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.preference_reset) + .setMessage(R.string.preference_reset_question) + .setPositiveButton(R.string.answer_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if( MyDebug.LOG ) + Log.d(TAG, "user confirmed reset"); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.putBoolean(PreferenceKeys.getFirstTimePreferenceKey(), true); + editor.apply(); + MainActivity main_activity = (MainActivity)MyPreferenceFragment.this.getActivity(); + main_activity.setDeviceDefaults(); + if( MyDebug.LOG ) + Log.d(TAG, "user clicked reset - need to restart"); + // see http://stackoverflow.com/questions/2470870/force-application-to-restart-on-first-activity + Intent i = getActivity().getBaseContext().getPackageManager().getLaunchIntentForPackage( getActivity().getBaseContext().getPackageName() ); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } + }) + .setNegativeButton(R.string.answer_no, null) + .show(); + } + return false; + } + }); + } + } + + public static class SaveFolderChooserDialog extends FolderChooserDialog { + @Override + public void onDismiss(DialogInterface dialog) { + if( MyDebug.LOG ) + Log.d(TAG, "FolderChooserDialog dismissed"); + // n.b., fragments have to be static (as they might be inserted into a new Activity - see http://stackoverflow.com/questions/15571010/fragment-inner-class-should-be-static), + // so we access the MainActivity via the fragment's getActivity(). + MainActivity main_activity = (MainActivity)this.getActivity(); + String new_save_location = this.getChosenFolder(); + main_activity.updateSaveFolder(new_save_location); + super.onDismiss(dialog); + } + } + + /*private void readFromBundle(Bundle bundle, String intent_key, String preference_key, String default_value, String preference_category_key) { + if( MyDebug.LOG ) { + Log.d(TAG, "readFromBundle: " + intent_key); + } + String [] values = bundle.getStringArray(intent_key); + if( values != null && values.length > 0 ) { + if( MyDebug.LOG ) { + Log.d(TAG, intent_key + " values:"); + for(int i=0;i> 16) & 0xFF; + int g = (backgroundColor >> 8) & 0xFF; + int b = (backgroundColor >> 0) & 0xFF; + Log.d(TAG, "backgroundColor: " + r + " , " + g + " , " + b); + }*/ + getView().setBackgroundColor(backgroundColor); + array.recycle(); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); + } + + public void onPause() { + super.onPause(); + } + + /* So that manual changes to the checkbox/switch preferences, while the preferences are showing, show up; + * in particular, needed for preference_using_saf, when the user cancels the SAF dialog (see + * MainActivity.onActivityResult). + */ + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if( MyDebug.LOG ) + Log.d(TAG, "onSharedPreferenceChanged"); + Preference pref = findPreference(key); + if( pref instanceof TwoStatePreference ){ + TwoStatePreference twoStatePref = (TwoStatePreference)pref; + twoStatePref.setChecked(prefs.getBoolean(key, true)); + } + } +} diff --git a/src/main/java/net/sourceforge/opencamera/MyTileService.java b/src/main/java/net/sourceforge/opencamera/MyTileService.java new file mode 100644 index 00000000..0188eed7 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyTileService.java @@ -0,0 +1,51 @@ +package net.sourceforge.opencamera; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.service.quicksettings.TileService; +import android.util.Log; + +/** Provides service for quick settings tile. + */ +@TargetApi(Build.VERSION_CODES.N) +public class MyTileService extends TileService { + private static final String TAG = "MyTileService"; + public static final String TILE_ID = "net.sourceforge.opencamera.TILE_CAMERA"; + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + } + + @Override + public void onStartListening() { + super.onStartListening(); + } + + @Override + public void onStopListening() { + super.onStopListening(); + } + + @Override + public void onClick() { + if( MyDebug.LOG ) + Log.d(TAG, "onClick"); + super.onClick(); + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setAction(TILE_ID); + startActivity(intent); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/MyTileServiceFrontCamera.java b/src/main/java/net/sourceforge/opencamera/MyTileServiceFrontCamera.java new file mode 100644 index 00000000..f9d8ad20 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyTileServiceFrontCamera.java @@ -0,0 +1,51 @@ +package net.sourceforge.opencamera; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.service.quicksettings.TileService; +import android.util.Log; + +/** Provides service for quick settings tile. + */ +@TargetApi(Build.VERSION_CODES.N) +public class MyTileServiceFrontCamera extends TileService { + private static final String TAG = "MyTileServiceFrontCam"; + public static final String TILE_ID = "net.sourceforge.opencamera.TILE_FRONT_CAMERA"; + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + } + + @Override + public void onStartListening() { + super.onStartListening(); + } + + @Override + public void onStopListening() { + super.onStopListening(); + } + + @Override + public void onClick() { + if( MyDebug.LOG ) + Log.d(TAG, "onClick"); + super.onClick(); + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setAction(TILE_ID); + startActivity(intent); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/MyTileServiceVideo.java b/src/main/java/net/sourceforge/opencamera/MyTileServiceVideo.java new file mode 100644 index 00000000..6272179e --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyTileServiceVideo.java @@ -0,0 +1,51 @@ +package net.sourceforge.opencamera; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.service.quicksettings.TileService; +import android.util.Log; + +/** Provides service for quick settings tile. + */ +@TargetApi(Build.VERSION_CODES.N) +public class MyTileServiceVideo extends TileService { + private static final String TAG = "MyTileServiceVideo"; + public static final String TILE_ID = "net.sourceforge.opencamera.TILE_VIDEO"; + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + } + + @Override + public void onStartListening() { + super.onStartListening(); + } + + @Override + public void onStopListening() { + super.onStopListening(); + } + + @Override + public void onClick() { + if( MyDebug.LOG ) + Log.d(TAG, "onClick"); + super.onClick(); + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setAction(TILE_ID); + startActivity(intent); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/MyWidgetProvider.java b/src/main/java/net/sourceforge/opencamera/MyWidgetProvider.java new file mode 100644 index 00000000..7fd93d48 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyWidgetProvider.java @@ -0,0 +1,84 @@ +package net.sourceforge.opencamera; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.MainActivity; +import net.sourceforge.opencamera.R; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.RemoteViews; + +/** Handles the Open Camera lock screen widget. Lock screen widgets are no + * longer supported in Android 5 onwards (instead Open Camera can be launched + * from the lock screen using the standard camera icon), but this is kept here + * for older Android versions. + */ +public class MyWidgetProvider extends AppWidgetProvider { + private static final String TAG = "MyWidgetProvider"; + + // from http://developer.android.com/guide/topics/appwidgets/index.html + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + if( MyDebug.LOG ) + Log.d(TAG, "onUpdate"); + if( MyDebug.LOG ) + Log.d(TAG, "length = " + appWidgetIds.length); + + // Perform this loop procedure for each App Widget that belongs to this provider + for(int appWidgetId : appWidgetIds) { + if( MyDebug.LOG ) + Log.d(TAG, "appWidgetId: " + appWidgetId); + + PendingIntent pendingIntent; + // for now, always put up the keyguard if the device is PIN locked etc + /*SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if( sharedPreferences.getBoolean(MainActivity.getShowWhenLockedPreferenceKey(), true) ) { + if( MyDebug.LOG ) + Log.d(TAG, "do show above lock screen"); + Intent intent = new Intent(context, MyWidgetProvider.class); + intent.setAction("net.sourceforge.opencamera.LAUNCH_OPEN_CAMERA"); + pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); + } + else*/ { + /*if( MyDebug.LOG ) + Log.d(TAG, "don't show above lock screen");*/ + Intent intent = new Intent(context, MainActivity.class); + pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + } + + // Get the layout for the App Widget and attach an on-click listener + // to the button + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); + views.setOnClickPendingIntent(R.id.widget_launch_open_camera, pendingIntent); + /*if( sharedPreferences.getBoolean(MainActivity.getShowWhenLockedPreferenceKey(), true) ) { + views.setTextViewText(R.id.launch_open_camera, "Open Camera (unlocked)"); + } + else { + views.setTextViewText(R.id.launch_open_camera, "Open Camera (locked)"); + }*/ + + // Tell the AppWidgetManager to perform an update on the current app widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + } + + /*@Override + public void onReceive(Context context, Intent intent) { + if( MyDebug.LOG ) { + Log.d(TAG, "onReceive " + intent); + } + if (intent.getAction().equals("net.sourceforge.opencamera.LAUNCH_OPEN_CAMERA")) { + if( MyDebug.LOG ) + Log.d(TAG, "Launching MainActivity"); + final Intent activity = new Intent(context, MainActivity.class); + activity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(activity); + if( MyDebug.LOG ) + Log.d(TAG, "done"); + } + super.onReceive(context, intent); + }*/ +} diff --git a/src/main/java/net/sourceforge/opencamera/MyWidgetProviderTakePhoto.java b/src/main/java/net/sourceforge/opencamera/MyWidgetProviderTakePhoto.java new file mode 100644 index 00000000..4e9c6f41 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/MyWidgetProviderTakePhoto.java @@ -0,0 +1,58 @@ +package net.sourceforge.opencamera; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.RemoteViews; + +/** Handles the Open Camera "take photo" widget. This widget launches Open + * Camera, and immediately takes a photo. + */ +public class MyWidgetProviderTakePhoto extends AppWidgetProvider { + private static final String TAG = "MyWidgetProviderTakePho"; + + // from http://developer.android.com/guide/topics/appwidgets/index.html + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + if( MyDebug.LOG ) + Log.d(TAG, "onUpdate"); + if( MyDebug.LOG ) + Log.d(TAG, "length = " + appWidgetIds.length); + + // Perform this loop procedure for each App Widget that belongs to this provider + for(int appWidgetId : appWidgetIds) { + if( MyDebug.LOG ) + Log.d(TAG, "appWidgetId: " + appWidgetId); + + Intent intent = new Intent(context, TakePhoto.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + + // Get the layout for the App Widget and attach an on-click listener + // to the button + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout_take_photo); + views.setOnClickPendingIntent(R.id.widget_take_photo, pendingIntent); + + // Tell the AppWidgetManager to perform an update on the current app widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + } + + /*@Override + public void onReceive(Context context, Intent intent) { + if( MyDebug.LOG ) { + Log.d(TAG, "onReceive " + intent); + } + if (intent.getAction().equals("net.sourceforge.opencamera.LAUNCH_OPEN_CAMERA")) { + if( MyDebug.LOG ) + Log.d(TAG, "Launching MainActivity"); + final Intent activity = new Intent(context, MainActivity.class); + activity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(activity); + if( MyDebug.LOG ) + Log.d(TAG, "done"); + } + super.onReceive(context, intent); + }*/ +} diff --git a/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java new file mode 100644 index 00000000..637155dc --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -0,0 +1,421 @@ +package net.sourceforge.opencamera; + +/** Stores all of the string keys used for SharedPreferences. + */ +public class PreferenceKeys { + // must be static, to safely call from other Activities + + // arguably the static methods here that don't receive an argument could just be static final strings? Though we may want to change some of them to be cameraId-specific in future + + /** If this preference is set, no longer show the intro dialog. + */ + public static String getFirstTimePreferenceKey() { + return "done_first_time"; + } + + /** If this preference is set, no longer show the auto-stabilise info dialog. + */ + public static String getAutoStabiliseInfoPreferenceKey() { + return "done_auto_stabilise_info"; + } + + /** If this preference is set, no longer show the HDR info dialog. + */ + public static String getHDRInfoPreferenceKey() { + return "done_hdr_info"; + } + + /** If this preference is set, no longer show the raw info dialog. + */ + public static String getRawInfoPreferenceKey() { + return "done_raw_info"; + } + + public static String getUseCamera2PreferenceKey() { + return "preference_use_camera2"; + } + + public static String getFlashPreferenceKey(int cameraId) { + return "flash_value_" + cameraId; + } + + public static String getFocusPreferenceKey(int cameraId, boolean is_video) { + return "focus_value_" + cameraId + "_" + is_video; + } + + public static String getResolutionPreferenceKey(int cameraId) { + return "camera_resolution_" + cameraId; + } + + public static String getVideoQualityPreferenceKey(int cameraId) { + return "video_quality_" + cameraId; + } + + public static String getIsVideoPreferenceKey() { + return "is_video"; + } + + public static String getExposurePreferenceKey() { + return "preference_exposure"; + } + + public static String getColorEffectPreferenceKey() { + return "preference_color_effect"; + } + + public static String getSceneModePreferenceKey() { + return "preference_scene_mode"; + } + + public static String getWhiteBalancePreferenceKey() { + return "preference_white_balance"; + } + + public static String getISOPreferenceKey() { + return "preference_iso"; + } + + public static String getExposureTimePreferenceKey() { + return "preference_exposure_time"; + } + + public static String getRawPreferenceKey() { + return "preference_raw"; + } + + public static String getExpoBracketingNImagesPreferenceKey() { + return "preference_expo_bracketing_n_images"; + } + + public static String getExpoBracketingStopsPreferenceKey() { + return "preference_expo_bracketing_stops"; + } + + public static String getVolumeKeysPreferenceKey() { + return "preference_volume_keys"; + } + + public static String getAudioControlPreferenceKey() { + return "preference_audio_control"; + } + + public static String getAudioNoiseControlSensitivityPreferenceKey() { + return "preference_audio_noise_control_sensitivity"; + } + + public static String getQualityPreferenceKey() { + return "preference_quality"; + } + + public static String getAutoStabilisePreferenceKey() { + return "preference_auto_stabilise"; + } + + public static String getPhotoModePreferenceKey() { + return "preference_photo_mode"; + } + + public static String getHDRSaveExpoPreferenceKey() { + return "preference_hdr_save_expo"; + } + + public static String getLocationPreferenceKey() { + return "preference_location"; + } + + public static String getGPSDirectionPreferenceKey() { + return "preference_gps_direction"; + } + + public static String getRequireLocationPreferenceKey() { + return "preference_require_location"; + } + + public static String getStampPreferenceKey() { + return "preference_stamp"; + } + + public static String getStampDateFormatPreferenceKey() { + return "preference_stamp_dateformat"; + } + + public static String getStampTimeFormatPreferenceKey() { + return "preference_stamp_timeformat"; + } + + public static String getStampGPSFormatPreferenceKey() { + return "preference_stamp_gpsformat"; + } + + public static String getTextStampPreferenceKey() { + return "preference_textstamp"; + } + + public static String getStampFontSizePreferenceKey() { + return "preference_stamp_fontsize"; + } + + public static String getStampFontColorPreferenceKey() { + return "preference_stamp_font_color"; + } + + public static String getStampStyleKey() { + return "preference_stamp_style"; + } + + public static String getVideoSubtitlePref() { + return "preference_video_subtitle"; + } + + public static String getFrontCameraMirrorKey() { + return "preference_front_camera_mirror"; + } + + public static String getBackgroundPhotoSavingPreferenceKey() { + return "preference_background_photo_saving"; + } + + public static String getCamera2FakeFlashPreferenceKey() { + return "preference_camera2_fake_flash"; + } + + public static String getCamera2FastBurstPreferenceKey() { + return "preference_camera2_fast_burst"; + } + + public static String getUIPlacementPreferenceKey() { + return "preference_ui_placement"; + } + + public static String getTouchCapturePreferenceKey() { + return "preference_touch_capture"; + } + + public static String getPausePreviewPreferenceKey() { + return "preference_pause_preview"; + } + + public static String getShowToastsPreferenceKey() { + return "preference_show_toasts"; + } + + public static String getThumbnailAnimationPreferenceKey() { + return "preference_thumbnail_animation"; + } + + public static String getTakePhotoBorderPreferenceKey() { + return "preference_take_photo_border"; + } + + public static String getShowWhenLockedPreferenceKey() { + return "preference_show_when_locked"; + } + + public static String getStartupFocusPreferenceKey() { + return "preference_startup_focus"; + } + + public static String getKeepDisplayOnPreferenceKey() { + return "preference_keep_display_on"; + } + + public static String getMaxBrightnessPreferenceKey() { + return "preference_max_brightness"; + } + + public static String getUsingSAFPreferenceKey() { + return "preference_using_saf"; + } + + public static String getSaveLocationPreferenceKey() { + return "preference_save_location"; + } + + public static String getSaveLocationSAFPreferenceKey() { + return "preference_save_location_saf"; + } + + public static String getSavePhotoPrefixPreferenceKey() { + return "preference_save_photo_prefix"; + } + + public static String getSaveVideoPrefixPreferenceKey() { + return "preference_save_video_prefix"; + } + + public static String getSaveZuluTimePreferenceKey() { + return "preference_save_zulu_time"; + } + + public static String getShowZoomControlsPreferenceKey() { + return "preference_show_zoom_controls"; + } + + public static String getShowZoomSliderControlsPreferenceKey() { + return "preference_show_zoom_slider_controls"; + } + + public static String getShowTakePhotoPreferenceKey() { + return "preference_show_take_photo"; + } + + public static String getShowZoomPreferenceKey() { + return "preference_show_zoom"; + } + + public static String getShowISOPreferenceKey() { + return "preference_show_iso"; + } + + public static String getShowAnglePreferenceKey() { + return "preference_show_angle"; + } + + public static String getShowAngleLinePreferenceKey() { + return "preference_show_angle_line"; + } + + public static String getShowPitchLinesPreferenceKey() { + return "preference_show_pitch_lines"; + } + + public static String getShowGeoDirectionLinesPreferenceKey() { + return "preference_show_geo_direction_lines"; + } + + public static String getShowAngleHighlightColorPreferenceKey() { + return "preference_angle_highlight_color"; + } + + public static String getCalibratedLevelAnglePreferenceKey() { + return "preference_calibrate_level_angle"; + } + + public static String getShowGeoDirectionPreferenceKey() { + return "preference_show_geo_direction"; + } + + public static String getShowFreeMemoryPreferenceKey() { + return "preference_free_memory"; + } + + public static String getShowTimePreferenceKey() { + return "preference_show_time"; + } + + public static String getShowBatteryPreferenceKey() { + return "preference_show_battery"; + } + + public static String getShowGridPreferenceKey() { + return "preference_grid"; + } + + public static String getShowCropGuidePreferenceKey() { + return "preference_crop_guide"; + } + + public static String getFaceDetectionPreferenceKey() { + return "preference_face_detection"; + } + + public static String getVideoStabilizationPreferenceKey() { + return "preference_video_stabilization"; + } + + public static String getForceVideo4KPreferenceKey() { + return "preference_force_video_4k"; + } + + public static String getVideoBitratePreferenceKey() { + return "preference_video_bitrate"; + } + + public static String getVideoFPSPreferenceKey() { + return "preference_video_fps"; + } + + public static String getVideoMaxDurationPreferenceKey() { + return "preference_video_max_duration"; + } + + public static String getVideoRestartPreferenceKey() { + return "preference_video_restart"; + } + + public static String getVideoDelayPreferenceKey() { + return "preference_video_delay_duration"; + } + + public static String getVideoMaxFileSizePreferenceKey() { + return "preference_video_max_filesize"; + } + + public static String getVideoRestartMaxFileSizePreferenceKey() { + return "preference_video_restart_max_filesize"; + } + + public static String getVideoFlashPreferenceKey() { + return "preference_video_flash"; + } + + public static String getVideoLowPowerCheckPreferenceKey() { + return "preference_video_low_power_check"; + } + + public static String getLockVideoPreferenceKey() { + return "preference_lock_video"; + } + + public static String getRecordAudioPreferenceKey() { + return "preference_record_audio"; + } + + public static String getRecordAudioChannelsPreferenceKey() { + return "preference_record_audio_channels"; + } + + public static String getRecordAudioSourcePreferenceKey() { + return "preference_record_audio_src"; + } + + public static String getPreviewSizePreferenceKey() { + return "preference_preview_size"; + } + + public static String getRotatePreviewPreferenceKey() { + return "preference_rotate_preview"; + } + + public static String getLockOrientationPreferenceKey() { + return "preference_lock_orientation"; + } + + public static String getTimerPreferenceKey() { + return "preference_timer"; + } + + public static String getTimerBeepPreferenceKey() { + return "preference_timer_beep"; + } + + public static String getTimerSpeakPreferenceKey() { + return "preference_timer_speak"; + } + + public static String getBurstModePreferenceKey() { + return "preference_burst_mode"; + } + + public static String getBurstIntervalPreferenceKey() { + return "preference_burst_interval"; + } + + public static String getShutterSoundPreferenceKey() { + return "preference_shutter_sound"; + } + + public static String getImmersiveModePreferenceKey() { + return "preference_immersive_mode"; + } +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/ApplicationInterface.java b/src/main/java/net/sourceforge/opencamera/Preview/ApplicationInterface.java new file mode 100644 index 00000000..ab5cabdb --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/ApplicationInterface.java @@ -0,0 +1,163 @@ +package net.sourceforge.opencamera.Preview; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.hardware.camera2.DngCreator; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.Image; +import android.net.Uri; +import android.util.Pair; +import android.view.MotionEvent; + +/** Provides communication between the Preview and the rest of the application + * - so in theory one can drop the Preview/ (and CameraController/) classes + * into a new application, by providing an appropriate implementation of this + * ApplicationInterface. + */ +public interface ApplicationInterface { + class NoFreeStorageException extends Exception { + private static final long serialVersionUID = -2021932609486148748L; + } + class VideoMaxFileSize { + public long max_filesize; // maximum file size in bytes for video (return 0 for device default - typically this is ~2GB) + public boolean auto_restart; // whether to automatically restart on hitting max filesize (this setting is still relevant for max_filesize==0, as typically there will still be a device max filesize) + } + + int VIDEOMETHOD_FILE = 0; // video will be saved to a file + int VIDEOMETHOD_SAF = 1; // video will be saved using Android 5's Storage Access Framework + int VIDEOMETHOD_URI = 2; // video will be written to the supplied Uri + + // methods that request information + Context getContext(); // get the application context + boolean useCamera2(); // should Android 5's Camera 2 API be used? + Location getLocation(); // get current location - null if not available (or you don't care about geotagging) + int createOutputVideoMethod(); // return a VIDEOMETHOD_* value to specify how to create a video file + File createOutputVideoFile() throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_FILE + Uri createOutputVideoSAF() throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_SAF + Uri createOutputVideoUri(); // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_URI + // for all of the get*Pref() methods, you can use Preview methods to get the supported values (e.g., getSupportedSceneModes()) + // if you just want a default or don't really care, see the comments for each method for a default or possible options + // if Preview doesn't support the requested setting, it will check this, and choose its own + int getCameraIdPref(); // camera to use, from 0 to getCameraControllerManager().getNumberOfCameras() + String getFlashPref(); // flash_off, flash_auto, flash_on, flash_torch, flash_red_eye + String getFocusPref(boolean is_video); // focus_mode_auto, focus_mode_infinity, focus_mode_macro, focus_mode_locked, focus_mode_fixed, focus_mode_manual2, focus_mode_edof, focus_mode_continuous_video + boolean isVideoPref(); // start up in video mode? + String getSceneModePref(); // "auto" for default (strings correspond to Android's scene mode constants in android.hardware.Camera.Parameters) + String getColorEffectPref(); // "node" for default (strings correspond to Android's color effect constants in android.hardware.Camera.Parameters) + String getWhiteBalancePref(); // "auto" for default (strings correspond to Android's white balance constants in android.hardware.Camera.Parameters) + String getISOPref(); // "auto" for auto-ISO, otherwise a numerical value; see documentation for Preview.supportsISORange(). + int getExposureCompensationPref(); // 0 for default + Pair getCameraResolutionPref(); // return null to let Preview choose size + int getImageQualityPref(); // jpeg quality for taking photos; "90" is a recommended default + boolean getFaceDetectionPref(); // whether to use face detection mode + String getVideoQualityPref(); // should be one of Preview.getSupportedVideoQuality() (use Preview.getCamcorderProfile() or Preview.getCamcorderProfileDescription() for details); or return "" to let Preview choose quality + boolean getVideoStabilizationPref(); // whether to use video stabilization for video + boolean getForce4KPref(); // whether to force 4K mode - experimental, only really available for some devices that allow 4K recording but don't return it as an available resolution - not recommended for most uses + String getVideoBitratePref(); // return "default" to let Preview choose + String getVideoFPSPref(); // return "default" to let Preview choose + long getVideoMaxDurationPref(); // time in ms after which to automatically stop video recording (return 0 for off) + int getVideoRestartTimesPref(); // number of times to restart video recording after hitting max duration (return 0 for never auto-restarting) + int getVideoDelayTimesPref(); //time for video delay + VideoMaxFileSize getVideoMaxFileSizePref() throws NoFreeStorageException; // see VideoMaxFileSize class for details + boolean getVideoFlashPref(); // option to switch flash on/off while recording video (should be false in most cases!) + boolean getVideoLowPowerCheckPref(); // whether to stop video automatically on critically low battery + String getPreviewSizePref(); // "preference_preview_size_wysiwyg" is recommended (preview matches aspect ratio of photo resolution as close as possible), but can also be "preference_preview_size_display" to maximise the preview size + String getPreviewRotationPref(); // return "0" for default; use "180" to rotate the preview 180 degrees + String getLockOrientationPref(); // return "none" for default; use "portrait" or "landscape" to lock photos/videos to that orientation + boolean getTouchCapturePref(); // whether to enable touch to capture + boolean getDoubleTapCapturePref(); // whether to enable double-tap to capture + boolean getPausePreviewPref(); // whether to pause the preview after taking a photo + boolean getShowToastsPref(); + boolean getShutterSoundPref(); // whether to play sound when taking photo + boolean getStartupFocusPref(); // whether to do autofocus on startup + long getTimerPref(); // time in ms for timer (so 0 for off) + String getRepeatPref(); // return number of times to repeat photo in a row (as a string), so "1" for default; return "unlimited" for unlimited + long getRepeatIntervalPref(); // time in ms between repeat + boolean getGeotaggingPref(); // whether to geotag photos + boolean getRequireLocationPref(); // if getGeotaggingPref() returns true, and this method returns true, then phot/video will only be taken if location data is available + boolean getRecordAudioPref(); // whether to record audio when recording video + String getRecordAudioChannelsPref(); // either "audio_default", "audio_mono" or "audio_stereo" + String getRecordAudioSourcePref(); // "audio_src_camcorder" is recommended, but other options are: "audio_src_mic", "audio_src_default", "audio_src_voice_communication"; see corresponding values in android.media.MediaRecorder.AudioSource + int getZoomPref(); // index into Preview.getSupportedZoomRatios() array (each entry is the zoom factor, scaled by 100; array is sorted from min to max zoom) + double getCalibratedLevelAngle(); // set to non-zero to calibrate the accelerometer used for the level angles + // Camera2 only modes: + long getExposureTimePref(); // only called if getISOPref() is not "default" + float getFocusDistancePref(); + boolean isExpoBracketingPref(); // whether to enable burst photos with expo bracketing + int getExpoBracketingNImagesPref(); // how many images to take for exposure bracketing + double getExpoBracketingStopsPref(); // stops per image for exposure bracketing + boolean getOptimiseAEForDROPref(); // see CameraController doc for setOptimiseAEForDRO(). + boolean isRawPref(); // whether to enable RAW photos + boolean useCamera2FakeFlash(); // whether to enable CameraController.setUseCamera2FakeFlash() for Camera2 API + boolean useCamera2FastBurst(); // whether to enable Camera2's captureBurst() for faster taking of expo-bracketing photos (generally should be true, but some devices have problems with captureBurst()) + + // for testing purposes: + boolean isTestAlwaysFocus(); // if true, pretend autofocus always successful + + // methods that transmit information/events (up to the Application whether to do anything or not) + void cameraSetup(); // called when the camera is (re-)set up - should update UI elements/parameters that depend on camera settings + void touchEvent(MotionEvent event); + void startingVideo(); // called just before video recording starts + void startedVideo(); // called just after video recording starts + void stoppingVideo(); // called just before video recording stops; note that if startingVideo() is called but then video recording fails to start, this method will still be called, but startedVideo() and stoppedVideo() won't be called + void stoppedVideo(final int video_method, final Uri uri, final String filename); // called after video recording stopped (uri/filename will be null if video is corrupt or not created); will be called iff startedVideo() was called + void onFailedStartPreview(); // called if failed to start camera preview + void onCameraError(); // called if the camera closes due to serious error. + void onPhotoError(); // callback for failing to take a photo + void onVideoInfo(int what, int extra); // callback for info when recording video (see MediaRecorder.OnInfoListener) + void onVideoError(int what, int extra); // callback for errors when recording video (see MediaRecorder.OnErrorListener) + void onVideoRecordStartError(CamcorderProfile profile); // callback for video recording failing to start + void onVideoRecordStopError(CamcorderProfile profile); // callback for video recording being corrupted + void onFailedReconnectError(); // failed to reconnect camera after stopping video recording + void onFailedCreateVideoFileError(); // callback if unable to create file for recording video + void hasPausedPreview(boolean paused); // called when the preview is paused or unpaused (due to getPausePreviewPref()) + void cameraInOperation(boolean in_operation); // called when the camera starts/stops being operation (taking photos or recording video, including if preview is paused after taking a photo), use to disable GUI elements during camera operation + void turnFrontScreenFlashOn(); // called when front-screen "flash" required (for modes flash_frontscreen_auto, flash_frontscreen_on); the application should light up the screen, until cameraInOperation(false) is called + void cameraClosed(); + void timerBeep(long remaining_time); // n.b., called once per second on timer countdown - so application can beep, or do whatever it likes + + // methods that request actions + void layoutUI(); // application should layout UI that's on top of the preview + void multitouchZoom(int new_zoom); // zoom has changed due to multitouch gesture on preview + // the set/clear*Pref() methods are called if Preview decides to override the requested pref (because Camera device doesn't support requested pref) (clear*Pref() is called if the feature isn't supported at all) + // the application can use this information to update its preferences + void setCameraIdPref(int cameraId); + void setFlashPref(String flash_value); + void setFocusPref(String focus_value, boolean is_video); + void setVideoPref(boolean is_video); + void setSceneModePref(String scene_mode); + void clearSceneModePref(); + void setColorEffectPref(String color_effect); + void clearColorEffectPref(); + void setWhiteBalancePref(String white_balance); + void clearWhiteBalancePref(); + void setISOPref(String iso); + void clearISOPref(); + void setExposureCompensationPref(int exposure); + void clearExposureCompensationPref(); + void setCameraResolutionPref(int width, int height); + void setVideoQualityPref(String video_quality); + void setZoomPref(int zoom); + void requestCameraPermission(); // for Android 6+: called when trying to open camera, but CAMERA permission not available + void requestStoragePermission(); // for Android 6+: called when trying to open camera, but WRITE_EXTERNAL_STORAGE permission not available + void requestRecordAudioPermission(); // for Android 6+: called when switching to (or starting up in) video mode, but RECORD_AUDIO permission not available + // Camera2 only modes: + void setExposureTimePref(long exposure_time); + void clearExposureTimePref(); + void setFocusDistancePref(float focus_distance); + + // callbacks + void onDrawPreview(Canvas canvas); + boolean onPictureTaken(byte [] data, Date current_date); + boolean onBurstPictureTaken(List images, Date current_date); + boolean onRawPictureTaken(DngCreator dngCreator, Image image, Date current_date); + void onCaptureStarted(); // called immediately before we start capturing the picture + void onPictureCompleted(); // called after all picture callbacks have been called and returned + void onContinuousFocusMove(boolean start); // called when focusing starts/stop in continuous picture mode (in photo mode only) +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/CameraSurface.java b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/CameraSurface.java new file mode 100644 index 00000000..9606f3a4 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/CameraSurface.java @@ -0,0 +1,17 @@ +package net.sourceforge.opencamera.Preview.CameraSurface; + +import net.sourceforge.opencamera.CameraController.CameraController; + +import android.graphics.Matrix; +import android.media.MediaRecorder; +import android.view.View; + +/** Provides support for the surface used for the preview - this can either be + * a SurfaceView or a TextureView. + */ +public interface CameraSurface { + View getView(); + void setPreviewDisplay(CameraController camera_controller); // n.b., uses double-dispatch similar to Visitor pattern - behaviour depends on type of CameraSurface and CameraController + void setVideoRecorder(MediaRecorder video_recorder); + void setTransform(Matrix matrix); +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MySurfaceView.java b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MySurfaceView.java new file mode 100644 index 00000000..3e6edf6e --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MySurfaceView.java @@ -0,0 +1,102 @@ +package net.sourceforge.opencamera.Preview.CameraSurface; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.CameraController.CameraControllerException; +import net.sourceforge.opencamera.Preview.Preview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.media.MediaRecorder; +import android.os.Handler; +import android.util.Log; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +/** Provides support for the surface used for the preview, using a SurfaceView. + */ +public class MySurfaceView extends SurfaceView implements CameraSurface { + private static final String TAG = "MySurfaceView"; + + private final Preview preview; + private final int [] measure_spec = new int[2]; + + @SuppressWarnings("deprecation") + public + MySurfaceView(Context context, Preview preview) { + super(context); + this.preview = preview; + if( MyDebug.LOG ) { + Log.d(TAG, "new MySurfaceView"); + } + + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed. + getHolder().addCallback(preview); + // deprecated setting, but required on Android versions prior to 3.0 + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // deprecated + + final Handler handler = new Handler(); + Runnable tick = new Runnable() { + public void run() { + /*if( MyDebug.LOG ) + Log.d(TAG, "invalidate()");*/ + invalidate(); + handler.postDelayed(this, 100); + } + }; + tick.run(); + } + + @Override + public View getView() { + return this; + } + + @Override + public void setPreviewDisplay(CameraController camera_controller) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewDisplay"); + try { + camera_controller.setPreviewDisplay(this.getHolder()); + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.e(TAG, "Failed to set preview display: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Override + public void setVideoRecorder(MediaRecorder video_recorder) { + video_recorder.setPreviewDisplay(this.getHolder().getSurface()); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return preview.touchEvent(event); + } + + @Override + public void onDraw(Canvas canvas) { + preview.draw(canvas); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + preview.getMeasureSpec(measure_spec, widthSpec, heightSpec); + super.onMeasure(measure_spec[0], measure_spec[1]); + } + + @Override + public void setTransform(Matrix matrix) { + if( MyDebug.LOG ) + Log.d(TAG, "setting transforms not supported for MySurfaceView"); + throw new RuntimeException(); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MyTextureView.java b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MyTextureView.java new file mode 100644 index 00000000..2ce91d91 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/CameraSurface/MyTextureView.java @@ -0,0 +1,82 @@ +package net.sourceforge.opencamera.Preview.CameraSurface; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.CameraController.CameraControllerException; +import net.sourceforge.opencamera.Preview.Preview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Matrix; +import android.media.MediaRecorder; +import android.util.Log; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; + +/** Provides support for the surface used for the preview, using a TextureView. + */ +public class MyTextureView extends TextureView implements CameraSurface { + private static final String TAG = "MyTextureView"; + + private final Preview preview; + private final int [] measure_spec = new int[2]; + + public MyTextureView(Context context, Preview preview) { + super(context); + this.preview = preview; + if( MyDebug.LOG ) { + Log.d(TAG, "new MyTextureView"); + } + + // Install a TextureView.SurfaceTextureListener so we get notified when the + // underlying surface is created and destroyed. + this.setSurfaceTextureListener(preview); + } + + @Override + public View getView() { + return this; + } + + @Override + public void setPreviewDisplay(CameraController camera_controller) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewDisplay"); + try { + camera_controller.setPreviewTexture(this.getSurfaceTexture()); + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.e(TAG, "Failed to set preview display: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Override + public void setVideoRecorder(MediaRecorder video_recorder) { + // should be no need to do anything (see documentation for MediaRecorder.setPreviewDisplay()) + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return preview.touchEvent(event); + } + + /*@Override + public void onDraw(Canvas canvas) { + preview.draw(canvas); + }*/ + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + preview.getMeasureSpec(measure_spec, widthSpec, heightSpec); + super.onMeasure(measure_spec[0], measure_spec[1]); + } + + @Override + public void setTransform(Matrix matrix) { + super.setTransform(matrix); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/CanvasView.java b/src/main/java/net/sourceforge/opencamera/Preview/CanvasView.java new file mode 100644 index 00000000..903009ff --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/CanvasView.java @@ -0,0 +1,55 @@ +package net.sourceforge.opencamera.Preview; + +import net.sourceforge.opencamera.MyDebug; +import android.content.Context; +import android.graphics.Canvas; +import android.os.Handler; +import android.util.Log; +import android.view.View; + +/** Overlay for the Preview - this just redirects to Preview.onDraw to do the + * work. Only used if using a MyTextureView (if using MySurfaceView, then that + * class can handle the onDraw()). TextureViews can't be used for both a + * camera preview, and used for drawing on. + */ +public class CanvasView extends View { + private static final String TAG = "CanvasView"; + + private final Preview preview; + private final int [] measure_spec = new int[2]; + + CanvasView(Context context, Preview preview) { + super(context); + this.preview = preview; + if( MyDebug.LOG ) { + Log.d(TAG, "new CanvasView"); + } + + // deprecated setting, but required on Android versions prior to 3.0 + //getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // deprecated + + final Handler handler = new Handler(); + Runnable tick = new Runnable() { + public void run() { + /*if( MyDebug.LOG ) + Log.d(TAG, "invalidate()");*/ + invalidate(); + handler.postDelayed(this, 100); + } + }; + tick.run(); + } + + @Override + public void onDraw(Canvas canvas) { + /*if( MyDebug.LOG ) + Log.d(TAG, "onDraw()");*/ + preview.draw(canvas); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + preview.getMeasureSpec(measure_spec, widthSpec, heightSpec); + super.onMeasure(measure_spec[0], measure_spec[1]); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/Preview.java b/src/main/java/net/sourceforge/opencamera/Preview/Preview.java new file mode 100644 index 00000000..8e947e41 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/Preview.java @@ -0,0 +1,5466 @@ +package net.sourceforge.opencamera.Preview; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.R; +import net.sourceforge.opencamera.TakePhoto; +import net.sourceforge.opencamera.ToastBoxer; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.CameraController.CameraController1; +import net.sourceforge.opencamera.CameraController.CameraController2; +import net.sourceforge.opencamera.CameraController.CameraControllerException; +import net.sourceforge.opencamera.CameraController.CameraControllerManager; +import net.sourceforge.opencamera.CameraController.CameraControllerManager1; +import net.sourceforge.opencamera.CameraController.CameraControllerManager2; +import net.sourceforge.opencamera.Preview.ApplicationInterface.NoFreeStorageException; +import net.sourceforge.opencamera.Preview.CameraSurface.CameraSurface; +import net.sourceforge.opencamera.Preview.CameraSurface.MySurfaceView; +import net.sourceforge.opencamera.Preview.CameraSurface.MyTextureView; + +import java.io.File; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.hardware.camera2.DngCreator; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.Image; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.Settings; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.util.Pair; +import android.view.Display; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.View.MeasureSpec; +import android.widget.Toast; + +/** This class was originally named due to encapsulating the camera preview, + * but in practice it's grown to more than this, and includes most of the + * operation of the camera. It exists at a higher level than CameraController + * (i.e., this isn't merely a low level wrapper to the camera API, but + * supports much of the Open Camera logic and functionality). Communication to + * the rest of the application is available through ApplicationInterface. + * We could probably do with decoupling this class into separate components! + */ +public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextureListener { + private static final String TAG = "Preview"; + + private final boolean using_android_l; + + private final ApplicationInterface applicationInterface; + private final CameraSurface cameraSurface; + private CanvasView canvasView; + private boolean set_preview_size; + private int preview_w, preview_h; + private boolean set_textureview_size; + private int textureview_w, textureview_h; + + private final Matrix camera_to_preview_matrix = new Matrix(); + private final Matrix preview_to_camera_matrix = new Matrix(); + //private RectF face_rect = new RectF(); + private double preview_targetRatio; + + //private boolean ui_placement_right = true; + + private boolean app_is_paused = true; + private boolean has_surface; + private boolean has_aspect_ratio; + private double aspect_ratio; + private final CameraControllerManager camera_controller_manager; + private CameraController camera_controller; + private boolean has_permissions = true; // whether we have permissions necessary to operate the camera (camera, storage); assume true until we've been denied one of them + private boolean is_video; + private volatile MediaRecorder video_recorder; // must be volatile for test project reading the state + private volatile boolean video_start_time_set; // must be volatile for test project reading the state + private long video_start_time; // when the video recording was started, or last resumed if it's was paused + private long video_accumulated_time; // this time should be added to (System.currentTimeMillis() - video_start_time) to find the true video duration, that takes into account pausing/resuming, as well as any auto-restarts from max filesize + private boolean video_recorder_is_paused; // whether video_recorder is running but has paused + private boolean video_restart_on_max_filesize; + private static final long min_safe_restart_video_time = 1000; // if the remaining max time after restart is less than this, don't restart + private int video_method = ApplicationInterface.VIDEOMETHOD_FILE; + private Uri video_uri; // for VIDEOMETHOD_SAF or VIDEOMETHOD_URI + private String video_filename; // for VIDEOMETHOD_FILE + + private static final int PHASE_NORMAL = 0; + private static final int PHASE_TIMER = 1; + private static final int PHASE_TAKING_PHOTO = 2; + private static final int PHASE_PREVIEW_PAUSED = 3; // the paused state after taking a photo + private volatile int phase = PHASE_NORMAL; // must be volatile for test project reading the state + private final Timer takePictureTimer = new Timer(); + private TimerTask takePictureTimerTask; + private final Timer beepTimer = new Timer(); + private TimerTask beepTimerTask; + private final Timer flashVideoTimer = new Timer(); + private TimerTask flashVideoTimerTask; + private final IntentFilter battery_ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + private final Timer batteryCheckVideoTimer = new Timer(); + private TimerTask batteryCheckVideoTimerTask; + private long take_photo_time; + private int remaining_burst_photos; + private int remaining_restart_video; + private int delay_time_video; + + private boolean is_preview_started; + + private int current_orientation; // orientation received by onOrientationChanged + private int current_rotation; // orientation relative to camera's orientation (used for parameters.setRotation()) + private boolean has_level_angle; + private double natural_level_angle; // "level" angle of device, before applying any calibration and without accounting for screen orientation + private double level_angle; // "level" angle of device, including calibration + private double orig_level_angle; // "level" angle of device, including calibration, but without accounting for screen orientation + private boolean has_pitch_angle; + private double pitch_angle; + + private boolean has_zoom; + private int max_zoom_factor; + private final GestureDetector gestureDetector; + private final ScaleGestureDetector scaleGestureDetector; + private List zoom_ratios; + private float minimum_focus_distance; + private boolean touch_was_multitouch; + private float touch_orig_x; + private float touch_orig_y; + + private List supported_flash_values; // our "values" format + private int current_flash_index = -1; // this is an index into the supported_flash_values array, or -1 if no flash modes available + + private List supported_focus_values; // our "values" format + private int current_focus_index = -1; // this is an index into the supported_focus_values array, or -1 if no focus modes available + private int max_num_focus_areas; + private boolean continuous_focus_move_is_started; + + private boolean is_exposure_lock_supported; + private boolean is_exposure_locked; + + private List color_effects; + private List scene_modes; + private List white_balances; + private List isos; + private boolean supports_iso_range; + private int min_iso; + private int max_iso; + private boolean supports_exposure_time; + private long min_exposure_time; + private long max_exposure_time; + private List exposures; + private int min_exposure; + private int max_exposure; + private float exposure_step; + private boolean supports_expo_bracketing; + private boolean supports_raw; + private float view_angle_x; + private float view_angle_y; + + private List supported_preview_sizes; + + private List sizes; + private int current_size_index = -1; // this is an index into the sizes array, or -1 if sizes not yet set + + private final VideoQualityHandler video_quality_handler = new VideoQualityHandler(); + + private Toast last_toast; + private final ToastBoxer flash_toast = new ToastBoxer(); + private final ToastBoxer focus_toast = new ToastBoxer(); + private final ToastBoxer take_photo_toast = new ToastBoxer(); + private final ToastBoxer pause_video_toast = new ToastBoxer(); + private final ToastBoxer seekbar_toast = new ToastBoxer(); + + private int ui_rotation; + + private boolean supports_face_detection; + private boolean using_face_detection; + private CameraController.Face [] faces_detected; + private boolean supports_video_stabilization; + private boolean can_disable_shutter_sound; + private boolean has_focus_area; + private int focus_screen_x; + private int focus_screen_y; + private long focus_complete_time = -1; + private long focus_started_time = -1; + private int focus_success = FOCUS_DONE; + private static final int FOCUS_WAITING = 0; + private static final int FOCUS_SUCCESS = 1; + private static final int FOCUS_FAILED = 2; + private static final int FOCUS_DONE = 3; + private String set_flash_value_after_autofocus = ""; + private boolean take_photo_after_autofocus; // set to take a photo when the in-progress autofocus has completed; if setting, remember to call camera_controller.setCaptureFollowAutofocusHint() + private boolean successfully_focused; + private long successfully_focused_time = -1; + + // accelerometer and geomagnetic sensor info + private static final float sensor_alpha = 0.8f; // for filter + private boolean has_gravity; + private final float [] gravity = new float[3]; + private boolean has_geomagnetic; + private final float [] geomagnetic = new float[3]; + private final float [] deviceRotation = new float[9]; + private final float [] cameraRotation = new float[9]; + private final float [] deviceInclination = new float[9]; + private boolean has_geo_direction; + private final float [] geo_direction = new float[3]; + private final float [] new_geo_direction = new float[3]; + + private final DecimalFormat decimal_format_1dp = new DecimalFormat("#.#"); + private final DecimalFormat decimal_format_2dp = new DecimalFormat("#.##"); + + /* If the user touches to focus in continuous mode, we switch the camera_controller to autofocus mode. + * autofocus_in_continuous_mode is set to true when this happens; the runnable reset_continuous_focus_runnable + * switches back to continuous mode. + */ + private final Handler reset_continuous_focus_handler = new Handler(); + private Runnable reset_continuous_focus_runnable; + private boolean autofocus_in_continuous_mode; + + // for testing; must be volatile for test project reading the state + private boolean is_test; // whether called from OpenCamera.test testing + public volatile int count_cameraStartPreview; + public volatile int count_cameraAutoFocus; + public volatile int count_cameraTakePicture; + public volatile int count_cameraContinuousFocusMoving; + public volatile boolean test_fail_open_camera; + public volatile boolean test_video_failure; + + public Preview(ApplicationInterface applicationInterface, ViewGroup parent) { + if( MyDebug.LOG ) { + Log.d(TAG, "new Preview"); + } + + this.applicationInterface = applicationInterface; + + Activity activity = (Activity)this.getContext(); + if( activity.getIntent() != null && activity.getIntent().getExtras() != null ) { + // whether called from testing + is_test = activity.getIntent().getExtras().getBoolean("test_project"); + if( MyDebug.LOG ) + Log.d(TAG, "is_test: " + is_test); + } + + this.using_android_l = applicationInterface.useCamera2(); + if( MyDebug.LOG ) { + Log.d(TAG, "using_android_l?: " + using_android_l); + } + + boolean using_texture_view = false; + if( using_android_l ) { + // use a TextureView for Android L - had bugs with SurfaceView not resizing properly on Nexus 7; and good to use a TextureView anyway + // ideally we'd use a TextureView for older camera API too, but sticking with SurfaceView to avoid risk of breaking behaviour + using_texture_view = true; + } + + if( using_texture_view ) { + this.cameraSurface = new MyTextureView(getContext(), this); + // a TextureView can't be used both as a camera preview, and used for drawing on, so we use a separate CanvasView + this.canvasView = new CanvasView(getContext(), this); + camera_controller_manager = new CameraControllerManager2(getContext()); + } + else { + this.cameraSurface = new MySurfaceView(getContext(), this); + camera_controller_manager = new CameraControllerManager1(); + } + + gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener()); + gestureDetector.setOnDoubleTapListener(new DoubleTapListener()); + scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); + + parent.addView(cameraSurface.getView()); + if( canvasView != null ) { + parent.addView(canvasView); + } + } + + /*private void previewToCamera(float [] coords) { + float alpha = coords[0] / (float)this.getWidth(); + float beta = coords[1] / (float)this.getHeight(); + coords[0] = 2000.0f * alpha - 1000.0f; + coords[1] = 2000.0f * beta - 1000.0f; + }*/ + + /*private void cameraToPreview(float [] coords) { + float alpha = (coords[0] + 1000.0f) / 2000.0f; + float beta = (coords[1] + 1000.0f) / 2000.0f; + coords[0] = alpha * (float)this.getWidth(); + coords[1] = beta * (float)this.getHeight(); + }*/ + + private Resources getResources() { + return cameraSurface.getView().getResources(); + } + + public View getView() { + return cameraSurface.getView(); + } + + // If this code is changed, important to test that face detection and touch to focus still works as expected, for front and back + // cameras, for old and new API, including with zoom. Also test with MainActivity.setWindowFlagsForCamera() setting orientation as SCREEN_ORIENTATION_REVERSE_LANDSCAPE, + // and/or set "Rotate preview" option to 180 degrees. + private void calculateCameraToPreviewMatrix() { + if( MyDebug.LOG ) + Log.d(TAG, "calculateCameraToPreviewMatrix"); + if( camera_controller == null ) + return; + camera_to_preview_matrix.reset(); + if( !using_android_l ) { + // from http://developer.android.com/reference/android/hardware/Camera.Face.html#rect + // Need mirror for front camera + boolean mirror = camera_controller.isFrontFacing(); + camera_to_preview_matrix.setScale(mirror ? -1 : 1, 1); + // This is the value for android.hardware.Camera.setDisplayOrientation. + int display_orientation = camera_controller.getDisplayOrientation(); + if( MyDebug.LOG ) { + Log.d(TAG, "orientation of display relative to camera orientaton: " + display_orientation); + } + camera_to_preview_matrix.postRotate(display_orientation); + } + else { + // Unfortunately the transformation for Android L API isn't documented, but this seems to work for Nexus 6. + // This is the equivalent code for android.hardware.Camera.setDisplayOrientation, but we don't actually use setDisplayOrientation() + // for CameraController2, so instead this is the equivalent code to https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int), + // except testing on Nexus 6 shows that we shouldn't change "result" for front facing camera. + boolean mirror = camera_controller.isFrontFacing(); + camera_to_preview_matrix.setScale(1, mirror ? -1 : 1); + int degrees = getDisplayRotationDegrees(); + int result = (camera_controller.getCameraOrientation() - degrees + 360) % 360; + if( MyDebug.LOG ) { + Log.d(TAG, "orientation of display relative to natural orientaton: " + degrees); + Log.d(TAG, "orientation of display relative to camera orientaton: " + result); + } + camera_to_preview_matrix.postRotate(result); + } + // Camera driver coordinates range from (-1000, -1000) to (1000, 1000). + // UI coordinates range from (0, 0) to (width, height). + camera_to_preview_matrix.postScale(cameraSurface.getView().getWidth() / 2000f, cameraSurface.getView().getHeight() / 2000f); + camera_to_preview_matrix.postTranslate(cameraSurface.getView().getWidth() / 2f, cameraSurface.getView().getHeight() / 2f); + } + + private void calculatePreviewToCameraMatrix() { + if( camera_controller == null ) + return; + calculateCameraToPreviewMatrix(); + if( !camera_to_preview_matrix.invert(preview_to_camera_matrix) ) { + if( MyDebug.LOG ) + Log.d(TAG, "calculatePreviewToCameraMatrix failed to invert matrix!?"); + } + } + + public Matrix getCameraToPreviewMatrix() { + calculateCameraToPreviewMatrix(); + return camera_to_preview_matrix; + } + + /*Matrix getPreviewToCameraMatrix() { + calculatePreviewToCameraMatrix(); + return preview_to_camera_matrix; + }*/ + + private ArrayList getAreas(float x, float y) { + float [] coords = {x, y}; + calculatePreviewToCameraMatrix(); + preview_to_camera_matrix.mapPoints(coords); + float focus_x = coords[0]; + float focus_y = coords[1]; + + int focus_size = 50; + if( MyDebug.LOG ) { + Log.d(TAG, "x, y: " + x + ", " + y); + Log.d(TAG, "focus x, y: " + focus_x + ", " + focus_y); + } + Rect rect = new Rect(); + rect.left = (int)focus_x - focus_size; + rect.right = (int)focus_x + focus_size; + rect.top = (int)focus_y - focus_size; + rect.bottom = (int)focus_y + focus_size; + if( rect.left < -1000 ) { + rect.left = -1000; + rect.right = rect.left + 2*focus_size; + } + else if( rect.right > 1000 ) { + rect.right = 1000; + rect.left = rect.right - 2*focus_size; + } + if( rect.top < -1000 ) { + rect.top = -1000; + rect.bottom = rect.top + 2*focus_size; + } + else if( rect.bottom > 1000 ) { + rect.bottom = 1000; + rect.top = rect.bottom - 2*focus_size; + } + + ArrayList areas = new ArrayList<>(); + areas.add(new CameraController.Area(rect, 1000)); + return areas; + } + + public boolean touchEvent(MotionEvent event) { + if( MyDebug.LOG ) + Log.d(TAG, "touch event at : " + event.getX() + " , " + event.getY() + " at time " + event.getEventTime()); + if( gestureDetector.onTouchEvent(event) ) { + if( MyDebug.LOG ) + Log.d(TAG, "touch event handled by gestureDetector"); + return true; + } + scaleGestureDetector.onTouchEvent(event); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "try to reopen camera due to touch"); + this.openCamera(); + return true; + } + applicationInterface.touchEvent(event); + /*if( MyDebug.LOG ) { + Log.d(TAG, "touch event: " + event.getAction()); + }*/ + if( event.getPointerCount() != 1 ) { + //multitouch_time = System.currentTimeMillis(); + touch_was_multitouch = true; + return true; + } + if( event.getAction() != MotionEvent.ACTION_UP ) { + if( event.getAction() == MotionEvent.ACTION_DOWN && event.getPointerCount() == 1 ) { + touch_was_multitouch = false; + if( event.getAction() == MotionEvent.ACTION_DOWN ) { + touch_orig_x = event.getX(); + touch_orig_y = event.getY(); + if( MyDebug.LOG ) + Log.d(TAG, "touch down at " + touch_orig_x + " , " + touch_orig_y); + } + } + return true; + } + // now only have to handle MotionEvent.ACTION_UP from this point onwards + + if( touch_was_multitouch ) { + return true; + } + if( !this.is_video && this.isTakingPhotoOrOnTimer() ) { + // if video, okay to refocus when recording + return true; + } + + // ignore swipes + { + float x = event.getX(); + float y = event.getY(); + float diff_x = x - touch_orig_x; + float diff_y = y - touch_orig_y; + float dist2 = diff_x*diff_x + diff_y*diff_y; + float scale = getResources().getDisplayMetrics().density; + float tol = 31 * scale + 0.5f; // convert dps to pixels (about 0.5cm) + if( MyDebug.LOG ) { + Log.d(TAG, "touched from " + touch_orig_x + " , " + touch_orig_y + " to " + x + " , " + y); + Log.d(TAG, "dist: " + Math.sqrt(dist2)); + Log.d(TAG, "tol: " + tol); + } + if( dist2 > tol*tol ) { + if( MyDebug.LOG ) + Log.d(TAG, "touch was a swipe"); + return true; + } + } + + // note, we always try to force start the preview (in case is_preview_paused has become false) + // except if recording video (firstly, the preview should be running; secondly, we don't want to reset the phase!) + if( !this.is_video ) { + startCameraPreview(); + } + cancelAutoFocus(); + + if( camera_controller != null && !this.using_face_detection ) { + this.has_focus_area = false; + ArrayList areas = getAreas(event.getX(), event.getY()); + if( camera_controller.setFocusAndMeteringArea(areas) ) { + if( MyDebug.LOG ) + Log.d(TAG, "set focus (and metering?) area"); + this.has_focus_area = true; + this.focus_screen_x = (int)event.getX(); + this.focus_screen_y = (int)event.getY(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "didn't set focus area in this mode, may have set metering"); + // don't set has_focus_area in this mode + } + } + + if( !this.is_video && applicationInterface.getTouchCapturePref() ) { + if( MyDebug.LOG ) + Log.d(TAG, "touch to capture"); + // interpret as if user had clicked take photo/video button, except that we set the focus/metering areas + this.takePicturePressed(); + return true; + } + + tryAutoFocus(false, true); + return true; + } + + //@SuppressLint("ClickableViewAccessibility") @Override + + /** Handle multitouch zoom. + */ + private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScale(ScaleGestureDetector detector) { + if( Preview.this.camera_controller != null && Preview.this.has_zoom ) { + Preview.this.scaleZoom(detector.getScaleFactor()); + } + return true; + } + } + + public boolean onDoubleTap() { + if( MyDebug.LOG ) + Log.d(TAG, "onDoubleTap()"); + if( !is_video && applicationInterface.getDoubleTapCapturePref() ) { + if( MyDebug.LOG ) + Log.d(TAG, "double-tap to capture"); + // interpret as if user had clicked take photo/video button (don't need to set focus/metering, as this was done in touchEvent() for the first touch of the double-tap) + takePicturePressed(); + } + return true; + } + + private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDoubleTap(MotionEvent e) { + if( MyDebug.LOG ) + Log.d(TAG, "onDoubleTap()"); + return Preview.this.onDoubleTap(); + } + } + + public void clearFocusAreas() { + if( MyDebug.LOG ) + Log.d(TAG, "clearFocusAreas()"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + // don't cancelAutoFocus() here, otherwise we get sluggish zoom behaviour on Camera2 API + camera_controller.clearFocusAndMetering(); + has_focus_area = false; + focus_success = FOCUS_DONE; + successfully_focused = false; + } + + public void getMeasureSpec(int [] spec, int widthSpec, int heightSpec) { + if( !this.hasAspectRatio() ) { + spec[0] = widthSpec; + spec[1] = heightSpec; + return; + } + double aspect_ratio = this.getAspectRatio(); + + int previewWidth = MeasureSpec.getSize(widthSpec); + int previewHeight = MeasureSpec.getSize(heightSpec); + + // Get the padding of the border background. + int hPadding = cameraSurface.getView().getPaddingLeft() + cameraSurface.getView().getPaddingRight(); + int vPadding = cameraSurface.getView().getPaddingTop() + cameraSurface.getView().getPaddingBottom(); + + // Resize the preview frame with correct aspect ratio. + previewWidth -= hPadding; + previewHeight -= vPadding; + + boolean widthLonger = previewWidth > previewHeight; + int longSide = (widthLonger ? previewWidth : previewHeight); + int shortSide = (widthLonger ? previewHeight : previewWidth); + if( longSide > shortSide * aspect_ratio ) { + longSide = (int) ((double) shortSide * aspect_ratio); + } + else { + shortSide = (int) ((double) longSide / aspect_ratio); + } + if( widthLonger ) { + previewWidth = longSide; + previewHeight = shortSide; + } + else { + previewWidth = shortSide; + previewHeight = longSide; + } + + // Add the padding of the border. + previewWidth += hPadding; + previewHeight += vPadding; + + spec[0] = MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY); + spec[1] = MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY); + } + + private void mySurfaceCreated() { + this.has_surface = true; + this.openCamera(); + } + + private void mySurfaceDestroyed() { + this.has_surface = false; + this.closeCamera(); + } + + private void mySurfaceChanged() { + // surface size is now changed to match the aspect ratio of camera preview - so we shouldn't change the preview to match the surface size, so no need to restart preview here + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + + // need to force a layoutUI update (e.g., so UI is oriented correctly when app goes idle, device is then rotated, and app is then resumed) + applicationInterface.layoutUI(); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if( MyDebug.LOG ) + Log.d(TAG, "surfaceCreated()"); + // The Surface has been created, acquire the camera and tell it where + // to draw. + mySurfaceCreated(); + cameraSurface.getView().setWillNotDraw(false); // see http://stackoverflow.com/questions/2687015/extended-surfaceviews-ondraw-method-never-called + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if( MyDebug.LOG ) + Log.d(TAG, "surfaceDestroyed()"); + // Surface will be destroyed when we return, so stop the preview. + // Because the CameraDevice object is not a shared resource, it's very + // important to release it when the activity is paused. + mySurfaceDestroyed(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + if( MyDebug.LOG ) + Log.d(TAG, "surfaceChanged " + w + ", " + h); + if( holder.getSurface() == null ) { + // preview surface does not exist + return; + } + mySurfaceChanged(); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture arg0, int width, int height) { + if( MyDebug.LOG ) + Log.d(TAG, "onSurfaceTextureAvailable()"); + this.set_textureview_size = true; + this.textureview_w = width; + this.textureview_h = height; + mySurfaceCreated(); + configureTransform(); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture arg0) { + if( MyDebug.LOG ) + Log.d(TAG, "onSurfaceTextureDestroyed()"); + this.set_textureview_size = false; + this.textureview_w = 0; + this.textureview_h = 0; + mySurfaceDestroyed(); + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture arg0, int width, int height) { + if( MyDebug.LOG ) + Log.d(TAG, "onSurfaceTextureSizeChanged " + width + ", " + height); + this.set_textureview_size = true; + this.textureview_w = width; + this.textureview_h = height; + mySurfaceChanged(); + configureTransform(); + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture arg0) { + } + + private void configureTransform() { + if( MyDebug.LOG ) + Log.d(TAG, "configureTransform"); + if( camera_controller == null || !this.set_preview_size || !this.set_textureview_size ) + return; + if( MyDebug.LOG ) + Log.d(TAG, "textureview size: " + textureview_w + ", " + textureview_h); + int rotation = getDisplayRotation(); + Matrix matrix = new Matrix(); + RectF viewRect = new RectF(0, 0, this.textureview_w, this.textureview_h); + RectF bufferRect = new RectF(0, 0, this.preview_h, this.preview_w); + float centerX = viewRect.centerX(); + float centerY = viewRect.centerY(); + if( Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation ) { + bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); + matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); + float scale = Math.max( + (float) textureview_h / preview_h, + (float) textureview_w / preview_w); + matrix.postScale(scale, scale, centerX, centerY); + matrix.postRotate(90 * (rotation - 2), centerX, centerY); + } + cameraSurface.setTransform(matrix); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void stopVideo(boolean from_restart) { + if( MyDebug.LOG ) + Log.d(TAG, "stopVideo()"); + if( video_recorder == null ) { + // no need to do anything if not recording + // (important to exit, otherwise we'll momentarily switch the take photo icon to video mode in MyApplicationInterface.stoppingVideo() when opening the settings in landscape mode + if( MyDebug.LOG ) + Log.d(TAG, "video wasn't recording anyway"); + return; + } + applicationInterface.stoppingVideo(); + if( flashVideoTimerTask != null ) { + flashVideoTimerTask.cancel(); + flashVideoTimerTask = null; + } + if( batteryCheckVideoTimerTask != null ) { + batteryCheckVideoTimerTask.cancel(); + batteryCheckVideoTimerTask = null; + } + if( !from_restart ) { + remaining_restart_video = 0; + } + if( video_recorder != null ) { // check again, just to be safe + if( MyDebug.LOG ) + Log.d(TAG, "stop video recording"); + /*is_taking_photo = false; + is_taking_photo_on_timer = false;*/ + this.phase = PHASE_NORMAL; + try { + video_recorder.setOnErrorListener(null); + video_recorder.setOnInfoListener(null); + if( MyDebug.LOG ) + Log.d(TAG, "about to call video_recorder.stop()"); + video_recorder.stop(); + if( MyDebug.LOG ) + Log.d(TAG, "done video_recorder.stop()"); + } + catch(RuntimeException e) { + // stop() can throw a RuntimeException if stop is called too soon after start - this indicates the video file is corrupt, and should be deleted + if( MyDebug.LOG ) + Log.d(TAG, "runtime exception when stopping video"); + if( video_method == ApplicationInterface.VIDEOMETHOD_SAF ) { + if( video_uri != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "delete corrupt video: " + video_uri); + DocumentsContract.deleteDocument(getContext().getContentResolver(), video_uri); + } + } + else if( video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + if( video_filename != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "delete corrupt video: " + video_filename); + File file = new File(video_filename); + if( !file.delete() ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to delete corrupt video: " + video_filename); + } + } + } + // else don't delete if a plain Uri + + video_method = ApplicationInterface.VIDEOMETHOD_FILE; + video_uri = null; + video_filename = null; + // if video recording is stopped quickly after starting, it's normal that we might not have saved a valid file, so no need to display a message + if( !video_start_time_set || System.currentTimeMillis() - video_start_time > 2000 ) { + CamcorderProfile profile = getCamcorderProfile(); + applicationInterface.onVideoRecordStopError(profile); + } + } + if( MyDebug.LOG ) + Log.d(TAG, "reset video_recorder"); + video_recorder.reset(); + if( MyDebug.LOG ) + Log.d(TAG, "release video_recorder"); + video_recorder.release(); + video_recorder = null; + video_recorder_is_paused = false; + reconnectCamera(false); // n.b., if something went wrong with video, then we reopen the camera - which may fail (or simply not reopen, e.g., if app is now paused) + applicationInterface.stoppedVideo(video_method, video_uri, video_filename); + video_method = ApplicationInterface.VIDEOMETHOD_FILE; + video_uri = null; + video_filename = null; + } + } + + private Context getContext() { + return applicationInterface.getContext(); + } + + /** Restart video - either due to hitting maximum filesize, or maximum duration. + */ + private void restartVideo(boolean due_to_max_filesize) { + if( MyDebug.LOG ) + Log.d(TAG, "restartVideo()"); + if( video_recorder != null ) { + if( due_to_max_filesize ) { + long last_time = System.currentTimeMillis() - video_start_time; + video_accumulated_time += last_time; + if( MyDebug.LOG ) { + Log.d(TAG, "last_time: " + last_time); + Log.d(TAG, "video_accumulated_time is now: " + video_accumulated_time); + } + } + else { + video_accumulated_time = 0; + } + stopVideo(true); // this will also stop the timertask + //for (int i=0;i<200000;i++){ + //System.out.println(i); + //} + + + + try { + Thread.sleep(delay_time_video*1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // handle restart + if( MyDebug.LOG ) { + if( due_to_max_filesize ) + Log.d(TAG, "restarting due to maximum filesize"); + else + Log.d(TAG, "remaining_restart_video is: " + remaining_restart_video); + } + if( due_to_max_filesize ) { + long video_max_duration = applicationInterface.getVideoMaxDurationPref(); + if( video_max_duration > 0 ) { + video_max_duration -= video_accumulated_time; + if( video_max_duration < min_safe_restart_video_time ) { + // if there's less than 1s to go, ignore it - don't want to risk the resultant video being corrupt or throwing error, due to stopping too soon + // so instead just pretend we hit the max duration instead + if( MyDebug.LOG ) + Log.d(TAG, "hit max filesize, but max time duration is also set, with remaining time less than 1s: " + video_max_duration); + due_to_max_filesize = false; + } + } + } + if( due_to_max_filesize || remaining_restart_video > 0 ) { + if( is_video ) { + String toast = null; + if( !due_to_max_filesize ) + toast = remaining_restart_video + " " + getContext().getResources().getString(R.string.repeats_to_go); + takePicture(due_to_max_filesize); + if( !due_to_max_filesize ) { + showToast(null, toast); // show the toast afterwards, as we're hogging the UI thread here, and media recorder takes time to start up + // must decrement after calling takePicture(), so that takePicture() doesn't reset the value of remaining_restart_video + remaining_restart_video--; + } + } + else { + remaining_restart_video = 0; + } + } + } + } + + private void reconnectCamera(boolean quiet) { + if( MyDebug.LOG ) + Log.d(TAG, "reconnectCamera()"); + if( camera_controller != null ) { // just to be safe + try { + camera_controller.reconnect(); + this.setPreviewPaused(false); + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to reconnect to camera"); + e.printStackTrace(); + applicationInterface.onFailedReconnectError(); + closeCamera(); + } + try { + tryAutoFocus(false, false); + } + catch(RuntimeException e) { + if( MyDebug.LOG ) + Log.e(TAG, "tryAutoFocus() threw exception: " + e.getMessage()); + e.printStackTrace(); + // this happens on Nexus 7 if trying to record video at bitrate 50Mbits or higher - it's fair enough that it fails, but we need to recover without a crash! + // not safe to call closeCamera, as any call to getParameters may cause a RuntimeException + // update: can no longer reproduce failures on Nexus 7?! + this.is_preview_started = false; + if( !quiet ) { + CamcorderProfile profile = getCamcorderProfile(); + applicationInterface.onVideoRecordStopError(profile); + } + camera_controller.release(); + camera_controller = null; + openCamera(); + } + } + } + + private void closeCamera() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "closeCamera()"); + debug_time = System.currentTimeMillis(); + } + removePendingContinuousFocusReset(); + has_focus_area = false; + focus_success = FOCUS_DONE; + focus_started_time = -1; + synchronized( this ) { + // synchronise for consistency (keep FindBugs happy) + take_photo_after_autofocus = false; + // no need to call camera_controller.setCaptureFollowAutofocusHint() as we're closing the camera + } + set_flash_value_after_autofocus = ""; + successfully_focused = false; + preview_targetRatio = 0.0; + // n.b., don't reset has_set_location, as we can remember the location when switching camera + if( continuous_focus_move_is_started ) { + continuous_focus_move_is_started = false; + applicationInterface.onContinuousFocusMove(false); + } + applicationInterface.cameraClosed(); + cancelTimer(); + if( camera_controller != null ) { + if( video_recorder != null ) { + stopVideo(false); + } + // make sure we're into continuous video mode for closing + // workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted + // so to be safe, we always reset to continuous video mode + this.updateFocusForVideo(); + // need to check for camera being non-null again - if an error occurred stopping the video, we will have closed the camera, and may not be able to reopen + if( camera_controller != null ) { + //camera.setPreviewCallback(null); + if( MyDebug.LOG ) { + Log.d(TAG, "closeCamera: about to pause preview: " + (System.currentTimeMillis() - debug_time)); + } + pausePreview(); + if( MyDebug.LOG ) { + Log.d(TAG, "closeCamera: about to release camera controller: " + (System.currentTimeMillis() - debug_time)); + } + camera_controller.release(); + camera_controller = null; + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "closeCamera: total time: " + (System.currentTimeMillis() - debug_time)); + } + } + + public void cancelTimer() { + if( MyDebug.LOG ) + Log.d(TAG, "cancelTimer()"); + if( this.isOnTimer() ) { + takePictureTimerTask.cancel(); + takePictureTimerTask = null; + if( beepTimerTask != null ) { + beepTimerTask.cancel(); + beepTimerTask = null; + } + /*is_taking_photo_on_timer = false; + is_taking_photo = false;*/ + this.phase = PHASE_NORMAL; + if( MyDebug.LOG ) + Log.d(TAG, "cancelled camera timer"); + } + } + + public void pausePreview() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "pausePreview()"); + debug_time = System.currentTimeMillis(); + } + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + // make sure we're into continuous video mode + // workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted + // so to be safe, we always reset to continuous video mode + // although I've now fixed this at the level where we close the settings, I've put this guard here, just in case the problem occurs from elsewhere + this.updateFocusForVideo(); + this.setPreviewPaused(false); + if( MyDebug.LOG ) { + Log.d(TAG, "pausePreview: about to stop preview: " + (System.currentTimeMillis() - debug_time)); + } + camera_controller.stopPreview(); + this.phase = PHASE_NORMAL; + this.is_preview_started = false; + if( MyDebug.LOG ) { + Log.d(TAG, "pausePreview: about to call cameraInOperation: " + (System.currentTimeMillis() - debug_time)); + } + applicationInterface.cameraInOperation(false); + if( MyDebug.LOG ) { + Log.d(TAG, "pausePreview: total time: " + (System.currentTimeMillis() - debug_time)); + } + } + + //private int debug_count_opencamera = 0; // see usage below + + /** Try to open the camera. Should only be called if camera_controller==null. + */ + private void openCamera() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "openCamera()"); + debug_time = System.currentTimeMillis(); + } + // need to init everything now, in case we don't open the camera (but these may already be initialised from an earlier call - e.g., if we are now switching to another camera) + // n.b., don't reset has_set_location, as we can remember the location when switching camera + is_preview_started = false; // theoretically should be false anyway, but I had one RuntimeException from surfaceCreated()->openCamera()->setupCamera()->setPreviewSize() because is_preview_started was true, even though the preview couldn't have been started + set_preview_size = false; + preview_w = 0; + preview_h = 0; + has_focus_area = false; + focus_success = FOCUS_DONE; + focus_started_time = -1; + synchronized( this ) { + // synchronise for consistency (keep FindBugs happy) + take_photo_after_autofocus = false; + // no need to call camera_controller.setCaptureFollowAutofocusHint() as we're opening the camera + } + set_flash_value_after_autofocus = ""; + successfully_focused = false; + preview_targetRatio = 0.0; + scene_modes = null; + has_zoom = false; + max_zoom_factor = 0; + minimum_focus_distance = 0.0f; + zoom_ratios = null; + faces_detected = null; + supports_face_detection = false; + using_face_detection = false; + supports_video_stabilization = false; + can_disable_shutter_sound = false; + color_effects = null; + white_balances = null; + isos = null; + supports_iso_range = false; + min_iso = 0; + max_iso = 0; + supports_exposure_time = false; + min_exposure_time = 0L; + max_exposure_time = 0L; + exposures = null; + min_exposure = 0; + max_exposure = 0; + exposure_step = 0.0f; + supports_expo_bracketing = false; + supports_raw = false; + view_angle_x = 55.0f; // set a sensible default + view_angle_y = 43.0f; // set a sensible default + sizes = null; + current_size_index = -1; + video_quality_handler.resetCurrentQuality(); + supported_flash_values = null; + current_flash_index = -1; + supported_focus_values = null; + current_focus_index = -1; + max_num_focus_areas = 0; + applicationInterface.cameraInOperation(false); + if( MyDebug.LOG ) + Log.d(TAG, "done showGUI"); + if( !this.has_surface ) { + if( MyDebug.LOG ) { + Log.d(TAG, "preview surface not yet available"); + } + return; + } + if( this.app_is_paused ) { + if( MyDebug.LOG ) { + Log.d(TAG, "don't open camera as app is paused"); + } + return; + } + + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + // we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() + if( MyDebug.LOG ) + Log.d(TAG, "check for permissions"); + if( ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera permission not available"); + has_permissions = false; + applicationInterface.requestCameraPermission(); + // return for now - the application should try to reopen the camera if permission is granted + return; + } + if( ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) { + if( MyDebug.LOG ) + Log.d(TAG, "storage permission not available"); + has_permissions = false; + applicationInterface.requestStoragePermission(); + // return for now - the application should try to reopen the camera if permission is granted + return; + } + if( MyDebug.LOG ) + Log.d(TAG, "permissions available"); + } + // set in case this was previously set to false + has_permissions = true; + + /*{ + // debug + if( debug_count_opencamera++ == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "debug: don't open camera yet"); + return; + } + }*/ + try { + int cameraId = applicationInterface.getCameraIdPref(); + if( cameraId < 0 || cameraId >= camera_controller_manager.getNumberOfCameras() ) { + if( MyDebug.LOG ) + Log.d(TAG, "invalid cameraId: " + cameraId); + cameraId = 0; + applicationInterface.setCameraIdPref(cameraId); + } + if( MyDebug.LOG ) { + Log.d(TAG, "try to open camera: " + cameraId); + Log.d(TAG, "openCamera: time before opening camera: " + (System.currentTimeMillis() - debug_time)); + } + if( test_fail_open_camera ) { + if( MyDebug.LOG ) + Log.d(TAG, "test failing to open camera"); + throw new CameraControllerException(); + } + CameraController.ErrorCallback cameraErrorCallback = new CameraController.ErrorCallback() { + public void onError() { + if( MyDebug.LOG ) + Log.e(TAG, "error from CameraController: camera device failed"); + if( camera_controller != null ) { + camera_controller = null; + applicationInterface.onCameraError(); + } + } + }; + if( using_android_l ) { + CameraController.ErrorCallback previewErrorCallback = new CameraController.ErrorCallback() { + public void onError() { + if( MyDebug.LOG ) + Log.e(TAG, "error from CameraController: preview failed to start"); + applicationInterface.onFailedStartPreview(); + } + }; + camera_controller = new CameraController2(this.getContext(), cameraId, previewErrorCallback, cameraErrorCallback); + if( applicationInterface.useCamera2FakeFlash() ) { + camera_controller.setUseCamera2FakeFlash(true); + } + } + else + camera_controller = new CameraController1(cameraId, cameraErrorCallback); + //throw new CameraControllerException(); // uncomment to test camera not opening + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.e(TAG, "Failed to open camera: " + e.getMessage()); + e.printStackTrace(); + camera_controller = null; + } + if( MyDebug.LOG ) { + Log.d(TAG, "openCamera: time after opening camera: " + (System.currentTimeMillis() - debug_time)); + } + boolean take_photo = false; + if( camera_controller != null ) { + Activity activity = (Activity)this.getContext(); + if( MyDebug.LOG ) + Log.d(TAG, "intent: " + activity.getIntent()); + if( activity.getIntent() != null && activity.getIntent().getExtras() != null ) { + take_photo = activity.getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO); + activity.getIntent().removeExtra(TakePhoto.TAKE_PHOTO); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no intent data"); + } + if( MyDebug.LOG ) + Log.d(TAG, "take_photo?: " + take_photo); + + this.setCameraDisplayOrientation(); + new OrientationEventListener(activity) { + @Override + public void onOrientationChanged(int orientation) { + Preview.this.onOrientationChanged(orientation); + } + }.enable(); + if( MyDebug.LOG ) { + Log.d(TAG, "openCamera: time after setting orientation: " + (System.currentTimeMillis() - debug_time)); + } + + if( MyDebug.LOG ) + Log.d(TAG, "call setPreviewDisplay"); + cameraSurface.setPreviewDisplay(camera_controller); + if( MyDebug.LOG ) { + Log.d(TAG, "openCamera: time after setting preview display: " + (System.currentTimeMillis() - debug_time)); + } + + setupCamera(take_photo); + } + + if( MyDebug.LOG ) { + Log.d(TAG, "openCamera: total time to open camera: " + (System.currentTimeMillis() - debug_time)); + } + } + + /** Try to reopen the camera, if not currently open (e.g., permission wasn't granted, but now it is). + */ + public void retryOpenCamera() { + if( MyDebug.LOG ) + Log.d(TAG, "retryOpenCamera()"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "try to reopen camera"); + this.openCamera(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "camera already open"); + } + } + + /** Returns false if we failed to open the camera because camera or storage permission wasn't available. + */ + public boolean hasPermissions() { + return has_permissions; + } + + /* Should only be called after camera first opened, or after preview is paused. + * take_photo is true if we have been called from the TakePhoto widget (which means + * we'll take a photo immediately after startup). + */ + public void setupCamera(boolean take_photo) { + if( MyDebug.LOG ) + Log.d(TAG, "setupCamera()"); + long debug_time = 0; + if( MyDebug.LOG ) { + debug_time = System.currentTimeMillis(); + } + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + boolean do_startup_focus = !take_photo && applicationInterface.getStartupFocusPref(); + if( MyDebug.LOG ) { + Log.d(TAG, "take_photo? " + take_photo); + Log.d(TAG, "do_startup_focus? " + do_startup_focus); + } + // make sure we're into continuous video mode for reopening + // workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted + // so to be safe, we always reset to continuous video mode + // although I've now fixed this at the level where we close the settings, I've put this guard here, just in case the problem occurs from elsewhere + // we'll switch to the user-requested focus by calling setFocusPref() from setupCameraParameters() below + this.updateFocusForVideo(); + + setupCameraParameters(); + + // now switch to video if saved + boolean saved_is_video = applicationInterface.isVideoPref(); + if( MyDebug.LOG ) { + Log.d(TAG, "saved_is_video: " + saved_is_video); + } + if( saved_is_video != this.is_video ) { + this.switchVideo(true); + } + + if( do_startup_focus && using_android_l && camera_controller.supportsAutoFocus() ) { + // need to switch flash off for autofocus - and for Android L, need to do this before starting preview (otherwise it won't work in time); for old camera API, need to do this after starting preview! + set_flash_value_after_autofocus = ""; + String old_flash_value = camera_controller.getFlashValue(); + // getFlashValue() may return "" if flash not supported! + // also set flash_torch - otherwise we get bug where torch doesn't turn on when starting up in video mode (and it's not like we want to turn torch off for startup focus, anyway) + if( old_flash_value.length() > 0 && !old_flash_value.equals("flash_off") && !old_flash_value.equals("flash_torch") ) { + set_flash_value_after_autofocus = old_flash_value; + camera_controller.setFlashValue("flash_off"); + } + if( MyDebug.LOG ) + Log.d(TAG, "set_flash_value_after_autofocus is now: " + set_flash_value_after_autofocus); + } + + if( this.supports_raw && applicationInterface.isRawPref() ) { + camera_controller.setRaw(true); + } + else { + camera_controller.setRaw(false); + } + + if( this.supports_expo_bracketing && applicationInterface.isExpoBracketingPref() ) { + camera_controller.setExpoBracketing(true); + camera_controller.setExpoBracketingNImages( applicationInterface.getExpoBracketingNImagesPref() ); + camera_controller.setExpoBracketingStops( applicationInterface.getExpoBracketingStopsPref() ); + // setUseExpoFastBurst called when taking a photo + } + else { + camera_controller.setExpoBracketing(false); + } + + camera_controller.setOptimiseAEForDRO( applicationInterface.getOptimiseAEForDROPref() ); + + // Must set preview size before starting camera preview + // and must do it after setting photo vs video mode + setPreviewSize(); // need to call this when we switch cameras, not just when we run for the first time + if( MyDebug.LOG ) { + Log.d(TAG, "setupCamera: time after setting preview size: " + (System.currentTimeMillis() - debug_time)); + } + // Must call startCameraPreview after checking if face detection is present - probably best to call it after setting all parameters that we want + startCameraPreview(); + if( MyDebug.LOG ) { + Log.d(TAG, "setupCamera: time after starting camera preview: " + (System.currentTimeMillis() - debug_time)); + } + + // must be done after setting parameters, as this function may set parameters + // also needs to be done after starting preview for some devices (e.g., Nexus 7) + if( this.has_zoom && applicationInterface.getZoomPref() != 0 ) { + zoomTo(applicationInterface.getZoomPref()); + if( MyDebug.LOG ) { + Log.d(TAG, "setupCamera: total time after zoomTo: " + (System.currentTimeMillis() - debug_time)); + } + } + + if( take_photo ) { + if( this.is_video ) { + this.switchVideo(false); // set during_startup to false, as we now need to reset the preview + } + } + + applicationInterface.cameraSetup(); // must call this after the above take_photo code for calling switchVideo + if( MyDebug.LOG ) { + Log.d(TAG, "setupCamera: total time after cameraSetup: " + (System.currentTimeMillis() - debug_time)); + } + + if( take_photo ) { + // take photo after a delay - otherwise we sometimes get a black image?! + // also need a longer delay for continuous picture focus, to allow a chance to focus - 1000ms seems to work okay for Nexus 6, put 1500ms to be safe + String focus_value = getCurrentFocusValue(); + final int delay = ( focus_value != null && focus_value.equals("focus_mode_continuous_picture") ) ? 1500 : 500; + if( MyDebug.LOG ) + Log.d(TAG, "delay for take photo: " + delay); + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "do automatic take picture"); + takePicture(false); + } + }, delay); + } + + if( do_startup_focus ) { + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "do startup autofocus"); + tryAutoFocus(true, false); // so we get the autofocus when starting up - we do this on a delay, as calling it immediately means the autofocus doesn't seem to work properly sometimes (at least on Galaxy Nexus) + } + }, 500); + } + + if( MyDebug.LOG ) { + Log.d(TAG, "setupCamera: total time after setupCamera: " + (System.currentTimeMillis() - debug_time)); + } + } + + private void setupCameraParameters() { + if( MyDebug.LOG ) + Log.d(TAG, "setupCameraParameters()"); + long debug_time = 0; + if( MyDebug.LOG ) { + debug_time = System.currentTimeMillis(); + } + { + // get available scene modes + // important, from docs: + // "Changing scene mode may override other parameters (such as flash mode, focus mode, white balance). + // For example, suppose originally flash mode is on and supported flash modes are on/off. In night + // scene mode, both flash mode and supported flash mode may be changed to off. After setting scene + // mode, applications should call getParameters to know if some parameters are changed." + if( MyDebug.LOG ) + Log.d(TAG, "set up scene mode"); + String value = applicationInterface.getSceneModePref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved scene mode: " + value); + + CameraController.SupportedValues supported_values = camera_controller.setSceneMode(value); + if( supported_values != null ) { + scene_modes = supported_values.values; + // now save, so it's available for PreferenceActivity + applicationInterface.setSceneModePref(supported_values.selected_value); + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearSceneModePref(); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after setting scene mode: " + (System.currentTimeMillis() - debug_time)); + } + + { + // grab all read-only info from parameters + if( MyDebug.LOG ) + Log.d(TAG, "grab info from parameters"); + CameraController.CameraFeatures camera_features = camera_controller.getCameraFeatures(); + this.has_zoom = camera_features.is_zoom_supported; + if( this.has_zoom ) { + this.max_zoom_factor = camera_features.max_zoom; + this.zoom_ratios = camera_features.zoom_ratios; + } + this.minimum_focus_distance = camera_features.minimum_focus_distance; + this.supports_face_detection = camera_features.supports_face_detection; + this.sizes = camera_features.picture_sizes; + supported_flash_values = camera_features.supported_flash_values; + supported_focus_values = camera_features.supported_focus_values; + this.max_num_focus_areas = camera_features.max_num_focus_areas; + this.is_exposure_lock_supported = camera_features.is_exposure_lock_supported; + this.supports_video_stabilization = camera_features.is_video_stabilization_supported; + this.can_disable_shutter_sound = camera_features.can_disable_shutter_sound; + this.supports_iso_range = camera_features.supports_iso_range; + this.min_iso = camera_features.min_iso; + this.max_iso = camera_features.max_iso; + this.supports_exposure_time = camera_features.supports_exposure_time; + this.min_exposure_time = camera_features.min_exposure_time; + this.max_exposure_time = camera_features.max_exposure_time; + this.min_exposure = camera_features.min_exposure; + this.max_exposure = camera_features.max_exposure; + this.exposure_step = camera_features.exposure_step; + this.supports_expo_bracketing = camera_features.supports_expo_bracketing; + this.supports_raw = camera_features.supports_raw; + this.view_angle_x = camera_features.view_angle_x; + this.view_angle_y = camera_features.view_angle_y; + this.video_quality_handler.setVideoSizes(camera_features.video_sizes); + this.supported_preview_sizes = camera_features.preview_sizes; + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after getting read only info: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up face detection"); + // get face detection supported + this.faces_detected = null; + if( this.supports_face_detection ) { + this.using_face_detection = applicationInterface.getFaceDetectionPref(); + } + else { + this.using_face_detection = false; + } + if( MyDebug.LOG ) { + Log.d(TAG, "supports_face_detection?: " + supports_face_detection); + Log.d(TAG, "using_face_detection?: " + using_face_detection); + } + if( this.using_face_detection ) { + class MyFaceDetectionListener implements CameraController.FaceDetectionListener { + @Override + public void onFaceDetection(CameraController.Face[] faces) { + faces_detected = new CameraController.Face[faces.length]; + System.arraycopy(faces, 0, faces_detected, 0, faces.length); + } + } + camera_controller.setFaceDetectionListener(new MyFaceDetectionListener()); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after setting face detection: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up video stabilization"); + if( this.supports_video_stabilization ) { + boolean using_video_stabilization = applicationInterface.getVideoStabilizationPref(); + if( MyDebug.LOG ) + Log.d(TAG, "using_video_stabilization?: " + using_video_stabilization); + camera_controller.setVideoStabilization(using_video_stabilization); + } + if( MyDebug.LOG ) + Log.d(TAG, "supports_video_stabilization?: " + supports_video_stabilization); + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after video stabilization: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up color effect"); + String value = applicationInterface.getColorEffectPref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved color effect: " + value); + + CameraController.SupportedValues supported_values = camera_controller.setColorEffect(value); + if( supported_values != null ) { + color_effects = supported_values.values; + // now save, so it's available for PreferenceActivity + applicationInterface.setColorEffectPref(supported_values.selected_value); + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearColorEffectPref(); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after color effect: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up white balance"); + String value = applicationInterface.getWhiteBalancePref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved white balance: " + value); + + CameraController.SupportedValues supported_values = camera_controller.setWhiteBalance(value); + if( supported_values != null ) { + white_balances = supported_values.values; + // now save, so it's available for PreferenceActivity + applicationInterface.setWhiteBalancePref(supported_values.selected_value); + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearWhiteBalancePref(); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after white balance: " + (System.currentTimeMillis() - debug_time)); + } + + // must be done before setting flash modes, as we may remove flash modes if in manual mode + if( MyDebug.LOG ) + Log.d(TAG, "set up iso"); + String value = applicationInterface.getISOPref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved iso: " + value); + boolean is_manual_iso = false; + if( supports_iso_range ) { + // in this mode, we can set any ISO value from min to max + this.isos = null; // if supports_iso_range==true, caller shouldn't be using getSupportedISOs() + + // now set the desired ISO mode/value + if( value.equals("auto") ) { + if( MyDebug.LOG ) + Log.d(TAG, "setting auto iso"); + camera_controller.setManualISO(false, 0); + } + else { + // try to parse the supplied manual ISO value + try { + if( MyDebug.LOG ) + Log.d(TAG, "setting manual iso"); + is_manual_iso = true; + int iso = Integer.parseInt(value); + if( MyDebug.LOG ) + Log.d(TAG, "iso: " + iso); + camera_controller.setManualISO(true, iso); + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "iso invalid format, can't parse to int"); + camera_controller.setManualISO(false, 0); + value = "auto"; // so we switch the preferences back to auto mode, rather than the invalid value + } + + // now save, so it's available for PreferenceActivity + applicationInterface.setISOPref(value); + } + } + else { + // in this mode, any support for ISO is only the specific ISOs offered by the CameraController + CameraController.SupportedValues supported_values = camera_controller.setISO(value); + if( supported_values != null ) { + isos = supported_values.values; + if( !supported_values.selected_value.equals("auto") ) { + if( MyDebug.LOG ) + Log.d(TAG, "has manual iso"); + is_manual_iso = true; + } + // now save, so it's available for PreferenceActivity + applicationInterface.setISOPref(supported_values.selected_value); + + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearISOPref(); + } + } + + if( is_manual_iso ) { + if( supports_exposure_time ) { + long exposure_time_value = applicationInterface.getExposureTimePref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved exposure_time: " + exposure_time_value); + if( exposure_time_value < min_exposure_time ) + exposure_time_value = min_exposure_time; + else if( exposure_time_value > max_exposure_time ) + exposure_time_value = max_exposure_time; + camera_controller.setExposureTime(exposure_time_value); + // now save + applicationInterface.setExposureTimePref(exposure_time_value); + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearExposureTimePref(); + } + + if( this.using_android_l && supported_flash_values != null ) { + // flash modes not supported when using Camera2 and manual ISO + // (it's unclear flash is useful - ideally we'd at least offer torch, but ISO seems to reset to 100 when flash/torch is on!) + supported_flash_values = null; + if( MyDebug.LOG ) + Log.d(TAG, "flash not supported in Camera2 manual mode"); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after manual iso: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) { + Log.d(TAG, "set up exposure compensation"); + Log.d(TAG, "min_exposure: " + min_exposure); + Log.d(TAG, "max_exposure: " + max_exposure); + } + // get min/max exposure + exposures = null; + if( min_exposure != 0 || max_exposure != 0 ) { + exposures = new ArrayList<>(); + for(int i=min_exposure;i<=max_exposure;i++) { + exposures.add("" + i); + } + // if in manual ISO mode, we still want to get the valid exposure compensations, but shouldn't set exposure compensation + if( !is_manual_iso ) { + int exposure = applicationInterface.getExposureCompensationPref(); + if( exposure < min_exposure || exposure > max_exposure ) { + exposure = 0; + if( MyDebug.LOG ) + Log.d(TAG, "saved exposure not supported, reset to 0"); + if( exposure < min_exposure || exposure > max_exposure ) { + if( MyDebug.LOG ) + Log.d(TAG, "zero isn't an allowed exposure?! reset to min " + min_exposure); + exposure = min_exposure; + } + } + camera_controller.setExposureCompensation(exposure); + // now save, so it's available for PreferenceActivity + applicationInterface.setExposureCompensationPref(exposure); + } + } + else { + // delete key in case it's present (e.g., if feature no longer available due to change in OS, or switching APIs) + applicationInterface.clearExposureCompensationPref(); + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after exposures: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up picture sizes"); + if( MyDebug.LOG ) { + for(int i=0;i resolution = applicationInterface.getCameraResolutionPref(); + if( resolution != null ) { + int resolution_w = resolution.first; + int resolution_h = resolution.second; + // now find size in valid list + for(int i=0;i current_size.width*current_size.height ) { + current_size_index = i; + current_size = size; + } + } + } + if( current_size_index != -1 ) { + CameraController.Size current_size = sizes.get(current_size_index); + if( MyDebug.LOG ) + Log.d(TAG, "Current size index " + current_size_index + ": " + current_size.width + ", " + current_size.height); + + // now save, so it's available for PreferenceActivity + applicationInterface.setCameraResolutionPref(current_size.width, current_size.height); + } + // size set later in setPreviewSize() + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after picture sizes: " + (System.currentTimeMillis() - debug_time)); + } + + { + int image_quality = applicationInterface.getImageQualityPref(); + if( MyDebug.LOG ) + Log.d(TAG, "set up jpeg quality: " + image_quality); + camera_controller.setJpegQuality(image_quality); + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after jpeg quality: " + (System.currentTimeMillis() - debug_time)); + } + + // get available sizes + initialiseVideoSizes(); + initialiseVideoQuality(); + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after video sizes: " + (System.currentTimeMillis() - debug_time)); + } + + String video_quality_value_s = applicationInterface.getVideoQualityPref(); + if( MyDebug.LOG ) + Log.d(TAG, "video_quality_value: " + video_quality_value_s); + video_quality_handler.setCurrentVideoQualityIndex(-1); + if( video_quality_value_s.length() > 0 ) { + // parse the saved video quality, and make sure it is still valid + // now find value in valid list + for(int i=0;i 0 ) { + // default to FullHD if available, else pick highest quality + // (FullHD will give smaller file sizes and generally give better performance than 4K so probably better for most users; also seems to suffer from less problems when using manual ISO in Camera2 API) + video_quality_handler.setCurrentVideoQualityIndex(0); // start with highest quality + for(int i=0;i 1 ) { + + String flash_value = applicationInterface.getFlashPref(); + if( flash_value.length() > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "found existing flash_value: " + flash_value); + if( !updateFlash(flash_value, false) ) { // don't need to save, as this is the value that's already saved + if( MyDebug.LOG ) + Log.d(TAG, "flash value no longer supported!"); + updateFlash(0, true); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "found no existing flash_value"); + // whilst devices with flash should support flash_auto, we'll also be in this codepath for front cameras with + // no flash, as instead the available options will be flash_off, flash_frontscreen_auto, flash_frontscreen_on + // see testTakePhotoFrontCameraScreenFlash + if( supported_flash_values.contains("flash_auto") ) + updateFlash("flash_auto", true); + else + updateFlash("flash_off", true); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "flash not supported"); + supported_flash_values = null; + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after setting up flash: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up focus"); + current_focus_index = -1; + if( supported_focus_values != null && supported_focus_values.size() > 1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "focus values: " + supported_focus_values); + + setFocusPref(true); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "focus not supported"); + supported_focus_values = null; + } + /*supported_focus_values = new ArrayList<>(); + supported_focus_values.add("focus_mode_auto"); + supported_focus_values.add("focus_mode_infinity"); + supported_focus_values.add("focus_mode_macro"); + supported_focus_values.add("focus_mode_locked"); + supported_focus_values.add("focus_mode_manual2"); + supported_focus_values.add("focus_mode_fixed"); + supported_focus_values.add("focus_mode_edof"); + supported_focus_values.add("focus_mode_continuous_video");*/ + /*View focusModeButton = (View) activity.findViewById(R.id.focus_mode); + focusModeButton.setVisibility(supported_focus_values != null && !immersive_mode ? View.VISIBLE : View.GONE);*/ + } + + { + float focus_distance_value = applicationInterface.getFocusDistancePref(); + if( MyDebug.LOG ) + Log.d(TAG, "saved focus_distance: " + focus_distance_value); + if( focus_distance_value < 0.0f ) + focus_distance_value = 0.0f; + else if( focus_distance_value > minimum_focus_distance ) + focus_distance_value = minimum_focus_distance; + camera_controller.setFocusDistance(focus_distance_value); + // now save + applicationInterface.setFocusDistancePref(focus_distance_value); + } + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: time after setting up focus: " + (System.currentTimeMillis() - debug_time)); + } + + { + if( MyDebug.LOG ) + Log.d(TAG, "set up exposure lock"); + // exposure lock should always default to false, as doesn't make sense to save it - we can't really preserve a "lock" after the camera is reopened + // also note that it isn't safe to lock the exposure before starting the preview + is_exposure_locked = false; + } + + if( MyDebug.LOG ) { + Log.d(TAG, "setupCameraParameters: total time for setting up camera parameters: " + (System.currentTimeMillis() - debug_time)); + } + } + + private void setPreviewSize() { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewSize()"); + // also now sets picture size + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + if( is_preview_started ) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewSize() shouldn't be called when preview is running"); + throw new RuntimeException(); // throw as RuntimeException, as this is a programming error + } + if( !using_android_l ) { + // don't do for Android L, else this means we get flash on startup autofocus if flash is on + this.cancelAutoFocus(); + } + // first set picture size (for photo mode, must be done now so we can set the picture size from this; for video, doesn't really matter when we set it) + CameraController.Size new_size = null; + if( this.is_video ) { + // In theory, the picture size shouldn't matter in video mode, but the stock Android camera sets a picture size + // which is the largest that matches the video's aspect ratio. + // This seems necessary to work around an aspect ratio bug introduced in Android 4.4.3 (on Nexus 7 at least): http://code.google.com/p/android/issues/detail?id=70830 + // which results in distorted aspect ratio on preview and recorded video! + CamcorderProfile profile = getCamcorderProfile(); + if( MyDebug.LOG ) + Log.d(TAG, "video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight); + double targetRatio = ((double)profile.videoFrameWidth) / (double)profile.videoFrameHeight; + new_size = getOptimalVideoPictureSize(sizes, targetRatio); + } + else { + if( current_size_index != -1 ) { + new_size = sizes.get(current_size_index); + } + } + if( new_size != null ) { + camera_controller.setPictureSize(new_size.width, new_size.height); + } + // set optimal preview size + if( supported_preview_sizes != null && supported_preview_sizes.size() > 0 ) { + /*CameraController.Size best_size = supported_preview_sizes.get(0); + for(CameraController.Size size : supported_preview_sizes) { + if( MyDebug.LOG ) + Log.d(TAG, " supported preview size: " + size.width + ", " + size.height); + if( size.width*size.height > best_size.width*best_size.height ) { + best_size = size; + } + }*/ + CameraController.Size best_size = getOptimalPreviewSize(supported_preview_sizes); + camera_controller.setPreviewSize(best_size.width, best_size.height); + this.set_preview_size = true; + this.preview_w = best_size.width; + this.preview_h = best_size.height; + this.setAspectRatio( ((double)best_size.width) / (double)best_size.height ); + } + } + + private void initialiseVideoSizes() { + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + this.video_quality_handler.sortVideoSizes(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void initialiseVideoQuality() { + int cameraId = camera_controller.getCameraId(); + List profiles = new ArrayList<>(); + List dimensions = new ArrayList<>(); + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + profiles.add(CamcorderProfile.QUALITY_HIGH); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + profiles.add(CamcorderProfile.QUALITY_2160P); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + profiles.add(CamcorderProfile.QUALITY_1080P); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + profiles.add(CamcorderProfile.QUALITY_720P); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + profiles.add(CamcorderProfile.QUALITY_480P); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_CIF) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_CIF); + profiles.add(CamcorderProfile.QUALITY_CIF); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + profiles.add(CamcorderProfile.QUALITY_QVGA); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QCIF) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QCIF); + profiles.add(CamcorderProfile.QUALITY_QCIF); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW) ) { + CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + profiles.add(CamcorderProfile.QUALITY_LOW); + dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight)); + } + this.video_quality_handler.initialiseVideoQualityFromProfiles(profiles, dimensions); + } + + private CamcorderProfile getCamcorderProfile(String quality) { + if( MyDebug.LOG ) + Log.d(TAG, "getCamcorderProfile(): " + quality); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return CamcorderProfile.get(0, CamcorderProfile.QUALITY_HIGH); + } + int cameraId = camera_controller.getCameraId(); + CamcorderProfile camcorder_profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); // default + try { + String profile_string = quality; + int index = profile_string.indexOf('_'); + if( index != -1 ) { + profile_string = quality.substring(0, index); + if( MyDebug.LOG ) + Log.d(TAG, " profile_string: " + profile_string); + } + int profile = Integer.parseInt(profile_string); + camcorder_profile = CamcorderProfile.get(cameraId, profile); + if( index != -1 && index+1 < quality.length() ) { + String override_string = quality.substring(index+1); + if( MyDebug.LOG ) + Log.d(TAG, " override_string: " + override_string); + if( override_string.charAt(0) == 'r' && override_string.length() >= 4 ) { + index = override_string.indexOf('x'); + if( index == -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "override_string invalid format, can't find x"); + } + else { + String resolution_w_s = override_string.substring(1, index); // skip first 'r' + String resolution_h_s = override_string.substring(index+1); + if( MyDebug.LOG ) { + Log.d(TAG, "resolution_w_s: " + resolution_w_s); + Log.d(TAG, "resolution_h_s: " + resolution_h_s); + } + // copy to local variable first, so that if we fail to parse height, we don't set the width either + int resolution_w = Integer.parseInt(resolution_w_s); + int resolution_h = Integer.parseInt(resolution_h_s); + camcorder_profile.videoFrameWidth = resolution_w; + camcorder_profile.videoFrameHeight = resolution_h; + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "unknown override_string initial code, or otherwise invalid format"); + } + } + } + catch(NumberFormatException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to parse video quality: " + quality); + e.printStackTrace(); + } + return camcorder_profile; + } + + public CamcorderProfile getCamcorderProfile() { + // 4K UHD video is not yet supported by Android API (at least testing on Samsung S5 and Note 3, they do not return it via getSupportedVideoSizes(), nor via a CamcorderProfile (either QUALITY_HIGH, or anything else) + // but it does work if we explicitly set the resolution (at least tested on an S5) + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return CamcorderProfile.get(0, CamcorderProfile.QUALITY_HIGH); + } + CamcorderProfile profile; + int cameraId = camera_controller.getCameraId(); + if( applicationInterface.getForce4KPref() ) { + if( MyDebug.LOG ) + Log.d(TAG, "force 4K UHD video"); + profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + profile.videoFrameWidth = 3840; + profile.videoFrameHeight = 2160; + profile.videoBitRate = (int)(profile.videoBitRate*2.8); // need a higher bitrate for the better quality - this is roughly based on the bitrate used by an S5's native camera app at 4K (47.6 Mbps, compared to 16.9 Mbps which is what's returned by the QUALITY_HIGH profile) + } + else if( this.video_quality_handler.getCurrentVideoQualityIndex() != -1 ) { + profile = getCamcorderProfile(this.video_quality_handler.getCurrentVideoQuality()); + } + else { + profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + + String bitrate_value = applicationInterface.getVideoBitratePref(); + if( !bitrate_value.equals("default") ) { + try { + int bitrate = Integer.parseInt(bitrate_value); + if( MyDebug.LOG ) + Log.d(TAG, "bitrate: " + bitrate); + profile.videoBitRate = bitrate; + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "bitrate invalid format, can't parse to int: " + bitrate_value); + } + } + String fps_value = applicationInterface.getVideoFPSPref(); + if( !fps_value.equals("default") ) { + try { + int fps = Integer.parseInt(fps_value); + if( MyDebug.LOG ) + Log.d(TAG, "fps: " + fps); + profile.videoFrameRate = fps; + } + catch(NumberFormatException exception) { + if( MyDebug.LOG ) + Log.d(TAG, "fps invalid format, can't parse to int: " + fps_value); + } + } + return profile; + } + + private static String formatFloatToString(final float f) { + final int i=(int)f; + if( f == i ) + return Integer.toString(i); + return String.format(Locale.getDefault(), "%.2f", f); + } + + private static int greatestCommonFactor(int a, int b) { + while( b > 0 ) { + int temp = b; + b = a % b; + a = temp; + } + return a; + } + + private static String getAspectRatio(int width, int height) { + int gcf = greatestCommonFactor(width, height); + if( gcf > 0 ) { + // had a Google Play crash due to gcf being 0!? Implies width must be zero + width /= gcf; + height /= gcf; + } + return width + ":" + height; + } + + public static String getMPString(int width, int height) { + float mp = (width*height)/1000000.0f; + return formatFloatToString(mp) + "MP"; + } + + public static String getAspectRatioMPString(int width, int height) { + return "(" + getAspectRatio(width, height) + ", " + getMPString(width, height) + ")"; + } + + public String getCamcorderProfileDescriptionShort(String quality) { + if( camera_controller == null ) + return ""; + CamcorderProfile profile = getCamcorderProfile(quality); + return profile.videoFrameWidth + "x" + profile.videoFrameHeight + " " + getMPString(profile.videoFrameWidth, profile.videoFrameHeight); + } + + public String getCamcorderProfileDescription(String quality) { + if( camera_controller == null ) + return ""; + CamcorderProfile profile = getCamcorderProfile(quality); + String highest = ""; + if( profile.quality == CamcorderProfile.QUALITY_HIGH ) { + highest = "Highest: "; + } + String type = ""; + if( profile.videoFrameWidth == 3840 && profile.videoFrameHeight == 2160 ) { + type = "4K Ultra HD "; + } + else if( profile.videoFrameWidth == 1920 && profile.videoFrameHeight == 1080 ) { + type = "Full HD "; + } + else if( profile.videoFrameWidth == 1280 && profile.videoFrameHeight == 720 ) { + type = "HD "; + } + else if( profile.videoFrameWidth == 720 && profile.videoFrameHeight == 480 ) { + type = "SD "; + } + else if( profile.videoFrameWidth == 640 && profile.videoFrameHeight == 480 ) { + type = "VGA "; + } + else if( profile.videoFrameWidth == 352 && profile.videoFrameHeight == 288 ) { + type = "CIF "; + } + else if( profile.videoFrameWidth == 320 && profile.videoFrameHeight == 240 ) { + type = "QVGA "; + } + else if( profile.videoFrameWidth == 176 && profile.videoFrameHeight == 144 ) { + type = "QCIF "; + } + return highest + type + profile.videoFrameWidth + "x" + profile.videoFrameHeight + " " + getAspectRatioMPString(profile.videoFrameWidth, profile.videoFrameHeight); + } + + public double getTargetRatio() { + return preview_targetRatio; + } + + private double calculateTargetRatioForPreview(Point display_size) { + double targetRatio; + String preview_size = applicationInterface.getPreviewSizePref(); + // should always use wysiwig for video mode, otherwise we get incorrect aspect ratio shown when recording video (at least on Galaxy Nexus, e.g., at 640x480) + // also not using wysiwyg mode with video caused corruption on Samsung cameras (tested with Samsung S3, Android 4.3, front camera, infinity focus) + if( preview_size.equals("preference_preview_size_wysiwyg") || this.is_video ) { + if( this.is_video ) { + if( MyDebug.LOG ) + Log.d(TAG, "set preview aspect ratio from video size (wysiwyg)"); + CamcorderProfile profile = getCamcorderProfile(); + if( MyDebug.LOG ) + Log.d(TAG, "video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight); + targetRatio = ((double)profile.videoFrameWidth) / (double)profile.videoFrameHeight; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "set preview aspect ratio from photo size (wysiwyg)"); + CameraController.Size picture_size = camera_controller.getPictureSize(); + if( MyDebug.LOG ) + Log.d(TAG, "picture_size: " + picture_size.width + " x " + picture_size.height); + targetRatio = ((double)picture_size.width) / (double)picture_size.height; + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "set preview aspect ratio from display size"); + // base target ratio from display size - means preview will fill the device's display as much as possible + // but if the preview's aspect ratio differs from the actual photo/video size, the preview will show a cropped version of what is actually taken + targetRatio = ((double)display_size.x) / (double)display_size.y; + } + this.preview_targetRatio = targetRatio; + if( MyDebug.LOG ) + Log.d(TAG, "targetRatio: " + targetRatio); + return targetRatio; + } + + private CameraController.Size getClosestSize(List sizes, double targetRatio) { + if( MyDebug.LOG ) + Log.d(TAG, "getClosestSize()"); + CameraController.Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + for(CameraController.Size size : sizes) { + double ratio = (double)size.width / size.height; + if( Math.abs(ratio - targetRatio) < minDiff ) { + optimalSize = size; + minDiff = Math.abs(ratio - targetRatio); + } + } + return optimalSize; + } + + public CameraController.Size getOptimalPreviewSize(List sizes) { + if( MyDebug.LOG ) + Log.d(TAG, "getOptimalPreviewSize()"); + final double ASPECT_TOLERANCE = 0.05; + if( sizes == null ) + return null; + CameraController.Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + Point display_size = new Point(); + Activity activity = (Activity)this.getContext(); + { + Display display = activity.getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + if( MyDebug.LOG ) + Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y); + } + double targetRatio = calculateTargetRatioForPreview(display_size); + int targetHeight = Math.min(display_size.y, display_size.x); + if( targetHeight <= 0 ) { + targetHeight = display_size.y; + } + // Try to find the size which matches the aspect ratio, and is closest match to display height + for(CameraController.Size size : sizes) { + if( MyDebug.LOG ) + Log.d(TAG, " supported preview size: " + size.width + ", " + size.height); + double ratio = (double)size.width / size.height; + if( Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE ) + continue; + if( Math.abs(size.height - targetHeight) < minDiff ) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + if( optimalSize == null ) { + // can't find match for aspect ratio, so find closest one + if( MyDebug.LOG ) + Log.d(TAG, "no preview size matches the aspect ratio"); + optimalSize = getClosestSize(sizes, targetRatio); + } + if( MyDebug.LOG ) { + Log.d(TAG, "chose optimalSize: " + optimalSize.width + " x " + optimalSize.height); + Log.d(TAG, "optimalSize ratio: " + ((double)optimalSize.width / optimalSize.height)); + } + return optimalSize; + } + + public CameraController.Size getOptimalVideoPictureSize(List sizes, double targetRatio) { + if( MyDebug.LOG ) + Log.d(TAG, "getOptimalVideoPictureSize()"); + final double ASPECT_TOLERANCE = 0.05; + if( sizes == null ) + return null; + CameraController.Size optimalSize = null; + // Try to find largest size that matches aspect ratio + for(CameraController.Size size : sizes) { + if( MyDebug.LOG ) + Log.d(TAG, " supported preview size: " + size.width + ", " + size.height); + double ratio = (double)size.width / size.height; + if( Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE ) + continue; + if( optimalSize == null || size.width > optimalSize.width ) { + optimalSize = size; + } + } + if( optimalSize == null ) { + // can't find match for aspect ratio, so find closest one + if( MyDebug.LOG ) + Log.d(TAG, "no picture size matches the aspect ratio"); + optimalSize = getClosestSize(sizes, targetRatio); + } + if( MyDebug.LOG ) { + Log.d(TAG, "chose optimalSize: " + optimalSize.width + " x " + optimalSize.height); + Log.d(TAG, "optimalSize ratio: " + ((double)optimalSize.width / optimalSize.height)); + } + return optimalSize; + } + + private void setAspectRatio(double ratio) { + if( ratio <= 0.0 ) + throw new IllegalArgumentException(); + + has_aspect_ratio = true; + if( aspect_ratio != ratio ) { + aspect_ratio = ratio; + if( MyDebug.LOG ) + Log.d(TAG, "new aspect ratio: " + aspect_ratio); + cameraSurface.getView().requestLayout(); + if( canvasView != null ) { + canvasView.requestLayout(); + } + } + } + + private boolean hasAspectRatio() { + return has_aspect_ratio; + } + + private double getAspectRatio() { + return aspect_ratio; + } + + /** Returns the ROTATION_* enum of the display relative to the natural device orientation. + */ + public int getDisplayRotation() { + // gets the display rotation (as a Surface.ROTATION_* constant), taking into account the getRotatePreviewPreferenceKey() setting + Activity activity = (Activity)this.getContext(); + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + + String rotate_preview = applicationInterface.getPreviewRotationPref(); + if( MyDebug.LOG ) + Log.d(TAG, " rotate_preview = " + rotate_preview); + if( rotate_preview.equals("180") ) { + switch (rotation) { + case Surface.ROTATION_0: rotation = Surface.ROTATION_180; break; + case Surface.ROTATION_90: rotation = Surface.ROTATION_270; break; + case Surface.ROTATION_180: rotation = Surface.ROTATION_0; break; + case Surface.ROTATION_270: rotation = Surface.ROTATION_90; break; + default: + break; + } + } + + return rotation; + } + + /** Returns the rotation in degrees of the display relative to the natural device orientation. + */ + private int getDisplayRotationDegrees() { + if( MyDebug.LOG ) + Log.d(TAG, "getDisplayRotationDegrees"); + int rotation = getDisplayRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + default: + break; + } + if( MyDebug.LOG ) + Log.d(TAG, " degrees = " + degrees); + return degrees; + } + + // for the Preview - from http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) + // note, if orientation is locked to landscape this is only called when setting up the activity, and will always have the same orientation + public void setCameraDisplayOrientation() { + if( MyDebug.LOG ) + Log.d(TAG, "setCameraDisplayOrientation()"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + if( using_android_l ) { + // need to configure the textureview + configureTransform(); + } + else { + int degrees = getDisplayRotationDegrees(); + if( MyDebug.LOG ) + Log.d(TAG, " degrees = " + degrees); + // note the code to make the rotation relative to the camera sensor is done in camera_controller.setDisplayOrientation() + camera_controller.setDisplayOrientation(degrees); + } + } + + // for taking photos - from http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int) + private void onOrientationChanged(int orientation) { + /*if( MyDebug.LOG ) { + Log.d(TAG, "onOrientationChanged()"); + Log.d(TAG, "orientation: " + orientation); + }*/ + if( orientation == OrientationEventListener.ORIENTATION_UNKNOWN ) + return; + if( camera_controller == null ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!");*/ + return; + } + orientation = (orientation + 45) / 90 * 90; + this.current_orientation = orientation % 360; + int new_rotation; + int camera_orientation = camera_controller.getCameraOrientation(); + if( camera_controller.isFrontFacing() ) { + new_rotation = (camera_orientation - orientation + 360) % 360; + } + else { + new_rotation = (camera_orientation + orientation) % 360; + } + if( new_rotation != current_rotation ) { + /*if( MyDebug.LOG ) { + Log.d(TAG, " current_orientation is " + current_orientation); + Log.d(TAG, " info orientation is " + camera_orientation); + Log.d(TAG, " set Camera rotation from " + current_rotation + " to " + new_rotation); + }*/ + this.current_rotation = new_rotation; + } + } + + private int getDeviceDefaultOrientation() { + WindowManager windowManager = (WindowManager)this.getContext().getSystemService(Context.WINDOW_SERVICE); + Configuration config = getResources().getConfiguration(); + int rotation = windowManager.getDefaultDisplay().getRotation(); + if( ( (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && + config.orientation == Configuration.ORIENTATION_LANDSCAPE ) + || ( (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && + config.orientation == Configuration.ORIENTATION_PORTRAIT ) ) { + return Configuration.ORIENTATION_LANDSCAPE; + } + else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + /* Returns the rotation (in degrees) to use for images/videos, taking the preference_lock_orientation into account. + */ + private int getImageVideoRotation() { + if( MyDebug.LOG ) + Log.d(TAG, "getImageVideoRotation() from current_rotation " + current_rotation); + String lock_orientation = applicationInterface.getLockOrientationPref(); + if( lock_orientation.equals("landscape") ) { + int camera_orientation = camera_controller.getCameraOrientation(); + int device_orientation = getDeviceDefaultOrientation(); + int result; + if( device_orientation == Configuration.ORIENTATION_PORTRAIT ) { + // should be equivalent to onOrientationChanged(270) + if( camera_controller.isFrontFacing() ) { + result = (camera_orientation + 90) % 360; + } + else { + result = (camera_orientation + 270) % 360; + } + } + else { + // should be equivalent to onOrientationChanged(0) + result = camera_orientation; + } + if( MyDebug.LOG ) + Log.d(TAG, "getImageVideoRotation() lock to landscape, returns " + result); + return result; + } + else if( lock_orientation.equals("portrait") ) { + int camera_orientation = camera_controller.getCameraOrientation(); + int result; + int device_orientation = getDeviceDefaultOrientation(); + if( device_orientation == Configuration.ORIENTATION_PORTRAIT ) { + // should be equivalent to onOrientationChanged(0) + result = camera_orientation; + } + else { + // should be equivalent to onOrientationChanged(90) + if( camera_controller.isFrontFacing() ) { + result = (camera_orientation + 270) % 360; + } + else { + result = (camera_orientation + 90) % 360; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "getImageVideoRotation() lock to portrait, returns " + result); + return result; + } + if( MyDebug.LOG ) + Log.d(TAG, "getImageVideoRotation() returns current_rotation " + current_rotation); + return this.current_rotation; + } + + public void draw(Canvas canvas) { + /*if( MyDebug.LOG ) + Log.d(TAG, "draw()");*/ + if( this.app_is_paused ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "draw(): app is paused");*/ + return; + } + /*if( true ) // test + return;*/ + /*if( MyDebug.LOG ) + Log.d(TAG, "ui_rotation: " + ui_rotation);*/ + /*if( MyDebug.LOG ) + Log.d(TAG, "canvas size " + canvas.getWidth() + " x " + canvas.getHeight());*/ + /*if( MyDebug.LOG ) + Log.d(TAG, "surface frame " + mHolder.getSurfaceFrame().width() + ", " + mHolder.getSurfaceFrame().height());*/ + + if( this.focus_success != FOCUS_DONE ) { + if( focus_complete_time != -1 && System.currentTimeMillis() > focus_complete_time + 1000 ) { + focus_success = FOCUS_DONE; + } + } + applicationInterface.onDrawPreview(canvas); + } + + public void scaleZoom(float scale_factor) { + if( MyDebug.LOG ) + Log.d(TAG, "scaleZoom() " + scale_factor); + if( this.camera_controller != null && this.has_zoom ) { + int zoom_factor = camera_controller.getZoom(); + float zoom_ratio = this.zoom_ratios.get(zoom_factor)/100.0f; + zoom_ratio *= scale_factor; + + int new_zoom_factor = zoom_factor; + if( zoom_ratio <= 1.0f ) { + new_zoom_factor = 0; + } + else if( zoom_ratio >= zoom_ratios.get(max_zoom_factor)/100.0f ) { + new_zoom_factor = max_zoom_factor; + } + else { + // find the closest zoom level + if( scale_factor > 1.0f ) { + // zooming in + for(int i=zoom_factor;i= zoom_ratio ) { + if( MyDebug.LOG ) + Log.d(TAG, "zoom int, found new zoom by comparing " + zoom_ratios.get(i)/100.0f + " >= " + zoom_ratio); + new_zoom_factor = i; + break; + } + } + } + else { + // zooming out + for(int i=zoom_factor;i>=0;i--) { + if( zoom_ratios.get(i)/100.0f <= zoom_ratio ) { + if( MyDebug.LOG ) + Log.d(TAG, "zoom out, found new zoom by comparing " + zoom_ratios.get(i)/100.0f + " <= " + zoom_ratio); + new_zoom_factor = i; + break; + } + } + } + } + if( MyDebug.LOG ) { + Log.d(TAG, "ScaleListener.onScale zoom_ratio is now " + zoom_ratio); + Log.d(TAG, " old zoom_factor " + zoom_factor + " ratio " + zoom_ratios.get(zoom_factor)/100.0f); + Log.d(TAG, " chosen new zoom_factor " + new_zoom_factor + " ratio " + zoom_ratios.get(new_zoom_factor)/100.0f); + } + zoomTo(new_zoom_factor); + applicationInterface.multitouchZoom(new_zoom_factor); + } + } + + public void zoomTo(int new_zoom_factor) { + if( MyDebug.LOG ) + Log.d(TAG, "ZoomTo(): " + new_zoom_factor); + if( new_zoom_factor < 0 ) + new_zoom_factor = 0; + else if( new_zoom_factor > max_zoom_factor ) + new_zoom_factor = max_zoom_factor; + // problem where we crashed due to calling this function with null camera should be fixed now, but check again just to be safe + if( camera_controller != null ) { + if( this.has_zoom ) { + // don't cancelAutoFocus() here, otherwise we get sluggish zoom behaviour on Camera2 API + camera_controller.setZoom(new_zoom_factor); + applicationInterface.setZoomPref(new_zoom_factor); + clearFocusAreas(); + } + } + } + + public void setFocusDistance(float new_focus_distance) { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusDistance: " + new_focus_distance); + if( camera_controller != null ) { + if( new_focus_distance < 0.0f ) + new_focus_distance = 0.0f; + else if( new_focus_distance > minimum_focus_distance ) + new_focus_distance = minimum_focus_distance; + if( camera_controller.setFocusDistance(new_focus_distance) ) { + // now save + applicationInterface.setFocusDistancePref(new_focus_distance); + { + String focus_distance_s; + if( new_focus_distance > 0.0f ) { + float real_focus_distance = 1.0f / new_focus_distance; + focus_distance_s = decimal_format_2dp.format(real_focus_distance) + getResources().getString(R.string.metres_abbreviation); + } + else { + focus_distance_s = getResources().getString(R.string.infinite); + } + showToast(seekbar_toast, getResources().getString(R.string.focus_distance) + " " + focus_distance_s); + } + } + } + } + + public void setExposure(int new_exposure) { + if( MyDebug.LOG ) + Log.d(TAG, "setExposure(): " + new_exposure); + if( camera_controller != null && ( min_exposure != 0 || max_exposure != 0 ) ) { + cancelAutoFocus(); + if( new_exposure < min_exposure ) + new_exposure = min_exposure; + else if( new_exposure > max_exposure ) + new_exposure = max_exposure; + if( camera_controller.setExposureCompensation(new_exposure) ) { + // now save + applicationInterface.setExposureCompensationPref(new_exposure); + showToast(seekbar_toast, getExposureCompensationString(new_exposure), 96); + } + } + } + + public void setISO(int new_iso) { + if( MyDebug.LOG ) + Log.d(TAG, "setISO(): " + new_iso); + if( camera_controller != null && supports_iso_range ) { + if( new_iso < min_iso ) + new_iso = min_iso; + else if( new_iso > max_iso ) + new_iso = max_iso; + if( camera_controller.setISO(new_iso) ) { + // now save + applicationInterface.setISOPref("" + new_iso); + showToast(seekbar_toast, getISOString(new_iso), 96); + } + } + } + + public void setExposureTime(long new_exposure_time) { + if( MyDebug.LOG ) + Log.d(TAG, "setExposureTime(): " + new_exposure_time); + if( camera_controller != null && supports_exposure_time ) { + if( new_exposure_time < min_exposure_time ) + new_exposure_time = min_exposure_time; + else if( new_exposure_time > max_exposure_time ) + new_exposure_time = max_exposure_time; + if( camera_controller.setExposureTime(new_exposure_time) ) { + // now save + applicationInterface.setExposureTimePref(new_exposure_time); + showToast(seekbar_toast, getExposureTimeString(new_exposure_time), 96); + } + } + } + + public String getExposureCompensationString(int exposure) { + float exposure_ev = exposure * exposure_step; + return getResources().getString(R.string.exposure_compensation) + " " + (exposure > 0 ? "+" : "") + decimal_format_2dp.format(exposure_ev) + " EV"; + } + + public String getISOString(int iso) { + return getResources().getString(R.string.iso) + " " + iso; + } + + public String getExposureTimeString(long exposure_time) { + double exposure_time_s = exposure_time/1000000000.0; + String string; + if( exposure_time >= 500000000 ) { + // show exposure times of more than 0.5s directly + string = decimal_format_1dp.format(exposure_time_s) + getResources().getString(R.string.seconds_abbreviation); + } + else { + double exposure_time_r = 1.0/exposure_time_s; + string = " 1/" + decimal_format_1dp.format(exposure_time_r) + getResources().getString(R.string.seconds_abbreviation); + } + return string; + } + + /*public String getFrameDurationString(long frame_duration) { + double frame_duration_s = frame_duration/1000000000.0; + double frame_duration_r = 1.0/frame_duration_s; + return getResources().getString(R.string.fps) + " " + decimal_format_1dp.format(frame_duration_r); + }*/ + + /*private String getFocusOneDistanceString(float dist) { + if( dist == 0.0f ) + return "inf."; + float real_dist = 1.0f/dist; + return decimal_format_2dp.format(real_dist) + getResources().getString(R.string.metres_abbreviation); + } + + public String getFocusDistanceString(float dist_min, float dist_max) { + String f_s = "f "; + //if( dist_min == dist_max ) + // return f_s + getFocusOneDistanceString(dist_min); + //return f_s + getFocusOneDistanceString(dist_min) + "-" + getFocusOneDistanceString(dist_max); + // just always show max for now + return f_s + getFocusOneDistanceString(dist_max); + }*/ + + public boolean canSwitchCamera() { + if( this.phase == PHASE_TAKING_PHOTO ) { + // just to be safe - risk of cancelling the autofocus before taking a photo, or otherwise messing things up + if( MyDebug.LOG ) + Log.d(TAG, "currently taking a photo"); + return false; + } + int n_cameras = camera_controller_manager.getNumberOfCameras(); + if( MyDebug.LOG ) + Log.d(TAG, "found " + n_cameras + " cameras"); + if( n_cameras == 0 ) + return false; + return true; + } + + public void setCamera(int cameraId) { + if( MyDebug.LOG ) + Log.d(TAG, "setCamera()"); + if( cameraId < 0 || cameraId >= camera_controller_manager.getNumberOfCameras() ) { + if( MyDebug.LOG ) + Log.d(TAG, "invalid cameraId: " + cameraId); + cameraId = 0; + } + if( canSwitchCamera() ) { + closeCamera(); + applicationInterface.setCameraIdPref(cameraId); + this.openCamera(); + } + } + + public static int [] matchPreviewFpsToVideo(List fps_ranges, int video_frame_rate) { + if( MyDebug.LOG ) + Log.d(TAG, "matchPreviewFpsToVideo()"); + int selected_min_fps = -1, selected_max_fps = -1, selected_diff = -1; + for(int [] fps_range : fps_ranges) { + if( MyDebug.LOG ) { + Log.d(TAG, " supported fps range: " + fps_range[0] + " to " + fps_range[1]); + } + int min_fps = fps_range[0]; + int max_fps = fps_range[1]; + if( min_fps <= video_frame_rate && max_fps >= video_frame_rate ) { + int diff = max_fps - min_fps; + if( selected_diff == -1 || diff < selected_diff ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + selected_diff = diff; + } + } + } + if( selected_min_fps != -1 ) { + if( MyDebug.LOG ) { + Log.d(TAG, " chosen fps range: " + selected_min_fps + " to " + selected_max_fps); + } + } + else { + selected_diff = -1; + int selected_dist = -1; + for(int [] fps_range : fps_ranges) { + int min_fps = fps_range[0]; + int max_fps = fps_range[1]; + int diff = max_fps - min_fps; + int dist; + if( max_fps < video_frame_rate ) + dist = video_frame_rate - max_fps; + else + dist = min_fps - video_frame_rate; + if( MyDebug.LOG ) { + Log.d(TAG, " supported fps range: " + min_fps + " to " + max_fps + " has dist " + dist + " and diff " + diff); + } + if( selected_dist == -1 || dist < selected_dist || ( dist == selected_dist && diff < selected_diff ) ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + selected_dist = dist; + selected_diff = diff; + } + } + if( MyDebug.LOG ) + Log.d(TAG, " can't find match for fps range, so choose closest: " + selected_min_fps + " to " + selected_max_fps); + } + return new int[]{selected_min_fps, selected_max_fps}; + } + + public static int [] chooseBestPreviewFps(List fps_ranges) { + if( MyDebug.LOG ) + Log.d(TAG, "chooseBestPreviewFps()"); + + // find value with lowest min that has max >= 30; if more than one of these, pick the one with highest max + int selected_min_fps = -1, selected_max_fps = -1; + for(int [] fps_range : fps_ranges) { + if( MyDebug.LOG ) { + Log.d(TAG, " supported fps range: " + fps_range[0] + " to " + fps_range[1]); + } + int min_fps = fps_range[0]; + int max_fps = fps_range[1]; + if( max_fps >= 30000 ) { + if( selected_min_fps == -1 || min_fps < selected_min_fps ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + } + else if( min_fps == selected_min_fps && max_fps > selected_max_fps ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + } + } + } + + if( selected_min_fps != -1 ) { + if( MyDebug.LOG ) { + Log.d(TAG, " chosen fps range: " + selected_min_fps + " to " + selected_max_fps); + } + } + else { + // just pick the widest range; if more than one, pick the one with highest max + int selected_diff = -1; + for(int [] fps_range : fps_ranges) { + int min_fps = fps_range[0]; + int max_fps = fps_range[1]; + int diff = max_fps - min_fps; + if( selected_diff == -1 || diff > selected_diff ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + selected_diff = diff; + } + else if( diff == selected_diff && max_fps > selected_max_fps ) { + selected_min_fps = min_fps; + selected_max_fps = max_fps; + selected_diff = diff; + } + } + if( MyDebug.LOG ) + Log.d(TAG, " can't find fps range 30fps or better, so picked widest range: " + selected_min_fps + " to " + selected_max_fps); + } + return new int[]{selected_min_fps, selected_max_fps}; + } + + /* It's important to set a preview FPS using chooseBestPreviewFps() rather than just leaving it to the default, as some devices + * have a poor choice of default - e.g., Nexus 5 and Nexus 6 on original Camera API default to (15000, 15000), which means very dark + * preview and photos in low light, as well as a less smooth framerate in good light. + * See http://stackoverflow.com/questions/18882461/why-is-the-default-android-camera-preview-smoother-than-my-own-camera-preview . + */ + private void setPreviewFps() { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewFps()"); + CamcorderProfile profile = getCamcorderProfile(); + List fps_ranges = camera_controller.getSupportedPreviewFpsRange(); + if( fps_ranges == null || fps_ranges.size() == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "fps_ranges not available"); + return; + } + int [] selected_fps; + if( this.is_video ) { + // For Nexus 5 and Nexus 6, we need to set the preview fps using matchPreviewFpsToVideo to avoid problem of dark preview in low light, as described above. + // When the video recording starts, the preview automatically adjusts, but still good to avoid too-dark preview before the user starts recording. + // However I'm wary of changing the behaviour for all devices at the moment, since some devices can be + // very picky about what works when it comes to recording video - e.g., corruption in preview or resultant video. + // So for now, I'm just fixing the Nexus 5/6 behaviour without changing behaviour for other devices. Later we can test on other devices, to see if we can + // use chooseBestPreviewFps() more widely. + // Update for v1.31: we no longer seem to need this - I no longer get a dark preview in photo or video mode if we don't set the fps range; + // but leaving the code as it is, to be safe. + boolean preview_too_dark = Build.MODEL.equals("Nexus 5") || Build.MODEL.equals("Nexus 6"); + String fps_value = applicationInterface.getVideoFPSPref(); + if( MyDebug.LOG ) { + Log.d(TAG, "preview_too_dark? " + preview_too_dark); + Log.d(TAG, "fps_value: " + fps_value); + } + if( fps_value.equals("default") && preview_too_dark ) { + selected_fps = chooseBestPreviewFps(fps_ranges); + } + else { + selected_fps = matchPreviewFpsToVideo(fps_ranges, profile.videoFrameRate*1000); + } + } + else { + // note that setting an fps here in continuous video focus mode causes preview to not restart after taking a photo on Galaxy Nexus + // but we need to do this, to get good light for Nexus 5 or 6 + // we could hardcode behaviour like we do for video, but this is the same way that Google Camera chooses preview fps for photos + // or I could hardcode behaviour for Galaxy Nexus, but since it's an old device (and an obscure bug anyway - most users don't really need continuous focus in photo mode), better to live with the bug rather than complicating the code + // Update for v1.29: this doesn't seem to happen on Galaxy Nexus with continuous picture focus mode, which is what we now use + // Update for v1.31: we no longer seem to need this - I no longer get a dark preview in photo or video mode if we don't set the fps range; + // but leaving the code as it is, to be safe. + selected_fps = chooseBestPreviewFps(fps_ranges); + } + camera_controller.setPreviewFpsRange(selected_fps[0], selected_fps[1]); + } + + public void switchVideo(boolean during_startup) { + if( MyDebug.LOG ) + Log.d(TAG, "switchVideo()"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + boolean old_is_video = is_video; + if( this.is_video ) { + if( video_recorder != null ) { + stopVideo(false); + } + this.is_video = false; + } + else { + if( this.isOnTimer() ) { + cancelTimer(); + this.is_video = true; + } + //else if( this.is_taking_photo ) { + else if( this.phase == PHASE_TAKING_PHOTO ) { + // wait until photo taken + if( MyDebug.LOG ) + Log.d(TAG, "wait until photo taken"); + } + else { + this.is_video = true; + } + } + + if( is_video != old_is_video ) { + setFocusPref(false); // first restore the saved focus for the new photo/video mode; don't do autofocus, as it'll be cancelled when restarting preview + /*if( !is_video ) { + // changing from video to photo mode + setFocusPref(false); // first restore the saved focus for the new photo/video mode; don't do autofocus, as it'll be cancelled when restarting preview + }*/ + + if( !during_startup ) { + // now save + applicationInterface.setVideoPref(is_video); + } + + if( !during_startup ) { + String focus_value = current_focus_index != -1 ? supported_focus_values.get(current_focus_index) : null; + if( MyDebug.LOG ) + Log.d(TAG, "focus_value is " + focus_value); + if( !is_video && focus_value != null && focus_value.equals("focus_mode_continuous_picture") ) { + if( MyDebug.LOG ) + Log.d(TAG, "restart camera due to returning to continuous picture mode from video mode"); + // workaround for bug on Nexus 6 at least where switching to video and back to photo mode causes continuous picture mode to stop + this.onPause(); + this.onResume(); + } + else { + if( this.is_preview_started ) { + camera_controller.stopPreview(); + this.is_preview_started = false; + } + setPreviewSize(); + // always start the camera preview, even if it was previously paused (also needed to update preview fps) + this.startCameraPreview(); + } + } + + /*if( is_video ) { + // changing from photo to video mode + setFocusPref(false); + }*/ + if( is_video ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { + // check for audio permission now, rather than when user starts video recording + // we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() + if( MyDebug.LOG ) + Log.d(TAG, "check for record audio permission"); + if( ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { + if( MyDebug.LOG ) + Log.d(TAG, "record audio permission not available"); + applicationInterface.requestRecordAudioPermission(); + // we can now carry on - if the user starts recording video, we'll check then if the permission was granted + } + } + } + } + } + + private boolean focusIsVideo() { + if( camera_controller != null ) { + return camera_controller.focusIsVideo(); + } + return false; + } + + private void setFocusPref(boolean auto_focus) { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusPref()"); + String focus_value = applicationInterface.getFocusPref(is_video); + if( focus_value.length() > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "found existing focus_value: " + focus_value); + if( !updateFocus(focus_value, true, false, auto_focus) ) { // don't need to save, as this is the value that's already saved + if( MyDebug.LOG ) + Log.d(TAG, "focus value no longer supported!"); + updateFocus(0, true, true, auto_focus); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "found no existing focus_value"); + updateFocus(is_video ? "focus_mode_continuous_video" : "focus_mode_auto", true, true, auto_focus); + } + } + + /** If in video mode, update the focus mode if necessary to be continuous video focus mode (if that mode is available). + * Normally we remember the user-specified focus value. And even setting the default is done in setFocusPref(). + * This method is used as a workaround for a bug on Samsung Galaxy S5 with UHD, where if the user switches to another + * (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the + * video is corrupted. + * @return If the focus mode is changed, this returns the previous focus mode; else it returns null. + */ + public String updateFocusForVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "updateFocusForVideo()"); + String old_focus_mode = null; + if( this.supported_focus_values != null && camera_controller != null && is_video ) { + boolean focus_is_video = focusIsVideo(); + if( MyDebug.LOG ) { + Log.d(TAG, "focus_is_video: " + focus_is_video + " , is_video: " + is_video); + } + if( focus_is_video != is_video ) { + if( MyDebug.LOG ) + Log.d(TAG, "need to change focus mode"); + old_focus_mode = this.getCurrentFocusValue(); + updateFocus(is_video ? "focus_mode_continuous_video" : "focus_mode_auto", true, false, false); // don't save, as we're just changing focus mode temporarily for the Samsung S5 video hack + } + } + return old_focus_mode; + } + + public String getErrorFeatures(CamcorderProfile profile) { + boolean was_4k = false, was_bitrate = false, was_fps = false; + if( profile.videoFrameWidth == 3840 && profile.videoFrameHeight == 2160 && applicationInterface.getForce4KPref() ) { + was_4k = true; + } + String bitrate_value = applicationInterface.getVideoBitratePref(); + if( !bitrate_value.equals("default") ) { + was_bitrate = true; + } + String fps_value = applicationInterface.getVideoFPSPref(); + if( !fps_value.equals("default") ) { + was_fps = true; + } + String features = ""; + if( was_4k || was_bitrate || was_fps ) { + if( was_4k ) { + features = "4K UHD"; + } + if( was_bitrate ) { + if( features.length() == 0 ) + features = "Bitrate"; + else + features += "/Bitrate"; + } + if( was_fps ) { + if( features.length() == 0 ) + features = "Frame rate"; + else + features += "/Frame rate"; + } + } + return features; + } + + public void updateFlash(String focus_value) { + if( MyDebug.LOG ) + Log.d(TAG, "updateFlash(): " + focus_value); + if( this.phase == PHASE_TAKING_PHOTO && !is_video ) { + // just to be safe - risk of cancelling the autofocus before taking a photo, or otherwise messing things up + if( MyDebug.LOG ) + Log.d(TAG, "currently taking a photo"); + return; + } + updateFlash(focus_value, true); + } + + private boolean updateFlash(String flash_value, boolean save) { + if( MyDebug.LOG ) + Log.d(TAG, "updateFlash(): " + flash_value); + if( supported_flash_values != null ) { + int new_flash_index = supported_flash_values.indexOf(flash_value); + if( MyDebug.LOG ) + Log.d(TAG, "new_flash_index: " + new_flash_index); + if( new_flash_index != -1 ) { + updateFlash(new_flash_index, save); + return true; + } + } + return false; + } + + private void updateFlash(int new_flash_index, boolean save) { + if( MyDebug.LOG ) + Log.d(TAG, "updateFlash(): " + new_flash_index); + // updates the Flash button, and Flash camera mode + if( supported_flash_values != null && new_flash_index != current_flash_index ) { + boolean initial = current_flash_index==-1; + current_flash_index = new_flash_index; + if( MyDebug.LOG ) + Log.d(TAG, " current_flash_index is now " + current_flash_index + " (initial " + initial + ")"); + + //Activity activity = (Activity)this.getContext(); + String [] flash_entries = getResources().getStringArray(R.array.flash_entries); + //String [] flash_icons = getResources().getStringArray(R.array.flash_icons); + String flash_value = supported_flash_values.get(current_flash_index); + if( MyDebug.LOG ) + Log.d(TAG, " flash_value: " + flash_value); + String [] flash_values = getResources().getStringArray(R.array.flash_values); + for(int i=0;i 0 ) { // check in case this isn't cancelled by time we take the photo + applicationInterface.timerBeep(remaining_time); + } + remaining_time -= 1000; + } + } + beepTimer.schedule(beepTimerTask = new BeepTimerTask(), 0, 1000); + } + + private void flashVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "flashVideo"); + // getFlashValue() may return "" if flash not supported! + String flash_value = camera_controller.getFlashValue(); + if( flash_value.length() == 0 ) + return; + String flash_value_ui = getCurrentFlashValue(); + if( flash_value_ui == null ) + return; + if( flash_value_ui.equals("flash_torch") ) + return; + if( flash_value.equals("flash_torch") ) { + // shouldn't happen? but set to what the UI is + cancelAutoFocus(); + camera_controller.setFlashValue(flash_value_ui); + return; + } + // turn on torch + cancelAutoFocus(); + camera_controller.setFlashValue("flash_torch"); + try { + Thread.sleep(100); + } + catch(InterruptedException e) { + e.printStackTrace(); + } + // turn off torch + cancelAutoFocus(); + camera_controller.setFlashValue(flash_value_ui); + } + + private void onVideoInfo(int what, int extra) { + if( MyDebug.LOG ) + Log.d(TAG, "onVideoInfo: " + what + " extra: " + extra); + if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED && video_restart_on_max_filesize ) { + if( MyDebug.LOG ) + Log.d(TAG, "restart due to max filesize reached"); + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + // but still need to check that the camera hasn't closed + if( camera_controller != null ) + restartVideo(true); + else { + if( MyDebug.LOG ) + Log.d(TAG, "don't restart video, as already cancelled"); + } + } + }); + } + else if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED ) { + if( MyDebug.LOG ) + Log.d(TAG, "reached max duration - see if we need to restart?"); + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + // but still need to check that the camera hasn't closed + if( camera_controller != null ) + restartVideo(false); // n.b., this will only restart if remaining_restart_video > 0 + else { + if( MyDebug.LOG ) + Log.d(TAG, "don't restart video, as already cancelled"); + } + } + }); + } + else if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED ) { + stopVideo(false); + } + applicationInterface.onVideoInfo(what, extra); // call this last, so that toasts show up properly (as we're hogging the UI thread here, and mediarecorder takes time to stop) + } + + private void onVideoError(int what, int extra) { + if( MyDebug.LOG ) + Log.d(TAG, "onVideoError: " + what + " extra: " + extra); + stopVideo(false); + applicationInterface.onVideoError(what, extra); // call this last, so that toasts show up properly (as we're hogging the UI thread here, and mediarecorder takes time to stop) + } + + /** Initiate "take picture" command. In video mode this means starting video command. In photo mode this may involve first + * autofocusing. + */ + private void takePicture(boolean max_filesize_restart) { + if( MyDebug.LOG ) + Log.d(TAG, "takePicture"); + //this.thumbnail_anim = false; + this.phase = PHASE_TAKING_PHOTO; + synchronized( this ) { + // synchronise for consistency (keep FindBugs happy) + take_photo_after_autofocus = false; + } + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + /*is_taking_photo_on_timer = false; + is_taking_photo = false;*/ + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + return; + } + if( !this.has_surface ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview surface not yet available"); + /*is_taking_photo_on_timer = false; + is_taking_photo = false;*/ + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + return; + } + + boolean store_location = applicationInterface.getGeotaggingPref(); + if( store_location ) { + boolean require_location = applicationInterface.getRequireLocationPref(); + if( require_location ) { + if( applicationInterface.getLocation() != null ) { + // fine, we have location + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "location data required, but not available"); + showToast(null, R.string.location_not_available); + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + return; + } + } + } + + if( is_video ) { + if( MyDebug.LOG ) + Log.d(TAG, "start video recording"); + startVideoRecording(max_filesize_restart); + return; + } + + takePhoto(false); + if( MyDebug.LOG ) + Log.d(TAG, "takePicture exit"); + } + + /** Start video recording. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void startVideoRecording(boolean max_filesize_restart) { + focus_success = FOCUS_DONE; // clear focus rectangle (don't do for taking photos yet) + // initialise just in case: + boolean created_video_file = false; + video_method = ApplicationInterface.VIDEOMETHOD_FILE; + video_uri = null; + video_filename = null; + ParcelFileDescriptor pfd_saf = null; + try { + video_method = applicationInterface.createOutputVideoMethod(); + if( MyDebug.LOG ) + Log.e(TAG, "video_method? " + video_method); + if( video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + File videoFile = applicationInterface.createOutputVideoFile(); + video_filename = videoFile.getAbsolutePath(); + created_video_file = true; + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + video_filename); + } + else { + if( video_method == ApplicationInterface.VIDEOMETHOD_SAF ) { + video_uri = applicationInterface.createOutputVideoSAF(); + } + else { + video_uri = applicationInterface.createOutputVideoUri(); + } + created_video_file = true; + if( MyDebug.LOG ) + Log.d(TAG, "save to: " + video_uri); + pfd_saf = getContext().getContentResolver().openFileDescriptor(video_uri, "rw"); + } + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't create media video file; check storage permissions?"); + e.printStackTrace(); + applicationInterface.onFailedCreateVideoFileError(); + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + } + if( created_video_file ) { + CamcorderProfile profile = getCamcorderProfile(); + if( MyDebug.LOG ) { + Log.d(TAG, "current_video_quality: " + this.video_quality_handler.getCurrentVideoQualityIndex()); + if( this.video_quality_handler.getCurrentVideoQualityIndex() != -1 ) + Log.d(TAG, "current_video_quality value: " + this.video_quality_handler.getCurrentVideoQuality()); + Log.d(TAG, "resolution " + profile.videoFrameWidth + " x " + profile.videoFrameHeight); + Log.d(TAG, "bit rate " + profile.videoBitRate); + } + + boolean enable_sound = applicationInterface.getShutterSoundPref(); + if( MyDebug.LOG ) + Log.d(TAG, "enable_sound? " + enable_sound); + camera_controller.enableShutterSound(enable_sound); // Camera2 API can disable video sound too + video_recorder = new MediaRecorder(); + this.camera_controller.unlock(); + if( MyDebug.LOG ) + Log.d(TAG, "set video listeners"); + video_recorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { + @Override + public void onInfo(MediaRecorder mr, int what, int extra) { + if( MyDebug.LOG ) + Log.d(TAG, "MediaRecorder info: " + what + " extra: " + extra); + final int final_what = what; + final int final_extra = extra; + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + onVideoInfo(final_what, final_extra); + } + }); + } + }); + video_recorder.setOnErrorListener(new MediaRecorder.OnErrorListener() { + public void onError(MediaRecorder mr, int what, int extra) { + final int final_what = what; + final int final_extra = extra; + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + onVideoError(final_what, final_extra); + } + }); + } + }); + camera_controller.initVideoRecorderPrePrepare(video_recorder); + boolean record_audio = applicationInterface.getRecordAudioPref(); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { + // needed for Android 6, in case users deny storage permission, otherwise we'll crash + // see https://developer.android.com/training/permissions/requesting.html + // we request permission when switching to video mode - if it wasn't granted, here we just switch it off + // we restrict check to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() + if( MyDebug.LOG ) + Log.e(TAG, "don't have RECORD_AUDIO permission"); + showToast(null, R.string.permission_record_audio_not_available); + record_audio = false; + } + if( record_audio ) { + String pref_audio_src = applicationInterface.getRecordAudioSourcePref(); + if( MyDebug.LOG ) + Log.d(TAG, "pref_audio_src: " + pref_audio_src); + int audio_source = MediaRecorder.AudioSource.CAMCORDER; + if( pref_audio_src.equals("audio_src_mic") ) { + audio_source = MediaRecorder.AudioSource.MIC; + } + else if( pref_audio_src.equals("audio_src_default") ) { + audio_source = MediaRecorder.AudioSource.DEFAULT; + } + else if( pref_audio_src.equals("audio_src_voice_communication") ) { + audio_source = MediaRecorder.AudioSource.VOICE_COMMUNICATION; + } + if( MyDebug.LOG ) + Log.d(TAG, "audio_source: " + audio_source); + video_recorder.setAudioSource(audio_source); + } + if( MyDebug.LOG ) + Log.d(TAG, "set video source"); + video_recorder.setVideoSource(using_android_l ? MediaRecorder.VideoSource.SURFACE : MediaRecorder.VideoSource.CAMERA); + + boolean store_location = applicationInterface.getGeotaggingPref(); + if( store_location && applicationInterface.getLocation() != null ) { + Location location = applicationInterface.getLocation(); + if( MyDebug.LOG ) { + Log.d(TAG, "set video location: lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy()); + } + video_recorder.setLocation((float)location.getLatitude(), (float)location.getLongitude()); + } + + if( MyDebug.LOG ) + Log.d(TAG, "set video profile"); + if( record_audio ) { + video_recorder.setProfile(profile); + String pref_audio_channels = applicationInterface.getRecordAudioChannelsPref(); + if( MyDebug.LOG ) + Log.d(TAG, "pref_audio_channels: " + pref_audio_channels); + if( pref_audio_channels.equals("audio_mono") ) { + video_recorder.setAudioChannels(1); + } + else if( pref_audio_channels.equals("audio_stereo") ) { + video_recorder.setAudioChannels(2); + } + } + else { + // from http://stackoverflow.com/questions/5524672/is-it-possible-to-use-camcorderprofile-without-audio-source + video_recorder.setOutputFormat(profile.fileFormat); + video_recorder.setVideoFrameRate(profile.videoFrameRate); + video_recorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight); + video_recorder.setVideoEncodingBitRate(profile.videoBitRate); + video_recorder.setVideoEncoder(profile.videoCodec); + } + if( MyDebug.LOG ) { + Log.d(TAG, "video fileformat: " + profile.fileFormat); + Log.d(TAG, "video framerate: " + profile.videoFrameRate); + Log.d(TAG, "video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight); + Log.d(TAG, "video bitrate: " + profile.videoBitRate); + Log.d(TAG, "video codec: " + profile.videoCodec); + } + boolean told_app_starting = false; // true if we called applicationInterface.startingVideo() + try { + ApplicationInterface.VideoMaxFileSize video_max_filesize = applicationInterface.getVideoMaxFileSizePref(); + long max_filesize = video_max_filesize.max_filesize; + //max_filesize = 15*1024*1024; // test + if( max_filesize > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "set max file size of: " + max_filesize); + try { + video_recorder.setMaxFileSize(max_filesize); + } + catch(RuntimeException e) { + // Google Camera warns this can happen - for example, if 64-bit filesizes not supported + if( MyDebug.LOG ) + Log.e(TAG, "failed to set max filesize of: " + max_filesize); + e.printStackTrace(); + } + } + video_restart_on_max_filesize = video_max_filesize.auto_restart; // note, we set this even if max_filesize==0, as it will still apply when hitting device max filesize limit + + // handle restart timer + long video_max_duration = applicationInterface.getVideoMaxDurationPref(); + if( MyDebug.LOG ) + Log.d(TAG, "user preference video_max_duration: " + video_max_duration); + if( max_filesize_restart ) { + if( video_max_duration > 0 ) { + video_max_duration -= video_accumulated_time; + // this should be greater or equal to min_safe_restart_video_time, as too short remaining time should have been caught in restartVideo() + if( video_max_duration < min_safe_restart_video_time ) { + if( MyDebug.LOG ) + Log.e(TAG, "trying to restart video with too short a time: " + video_max_duration); + video_max_duration = min_safe_restart_video_time; + } + } + } + else { + video_accumulated_time = 0; + } + if( MyDebug.LOG ) + Log.d(TAG, "actual video_max_duration: " + video_max_duration); + video_recorder.setMaxDuration((int)video_max_duration); + + if( video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + video_recorder.setOutputFile(video_filename); + } + else { + video_recorder.setOutputFile(pfd_saf.getFileDescriptor()); + } + + applicationInterface.cameraInOperation(true); + told_app_starting = true; + applicationInterface.startingVideo(); + /*if( true ) // test + throw new IOException();*/ + cameraSurface.setVideoRecorder(video_recorder); + video_recorder.setOrientationHint(getImageVideoRotation()); + if( MyDebug.LOG ) + Log.d(TAG, "about to prepare video recorder"); + video_recorder.prepare(); + camera_controller.initVideoRecorderPostPrepare(video_recorder); + if( MyDebug.LOG ) + Log.d(TAG, "about to start video recorder"); + video_recorder.start(); + video_recorder_is_paused = false; + if( MyDebug.LOG ) + Log.d(TAG, "video recorder started"); + if( test_video_failure ) { + if( MyDebug.LOG ) + Log.d(TAG, "test_video_failure is true"); + throw new RuntimeException(); + } + video_start_time = System.currentTimeMillis(); + video_start_time_set = true; + applicationInterface.startedVideo(); + // Don't send intent for ACTION_MEDIA_SCANNER_SCAN_FILE yet - wait until finished, so we get completed file. + // Don't do any further calls after applicationInterface.startedVideo() that might throw an error - instead video error + // should be handled by including a call to stopVideo() (since the video_recorder has started). + } + catch(IOException e) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to save video"); + e.printStackTrace(); + if( told_app_starting ) { + applicationInterface.stoppingVideo(); + } + applicationInterface.onFailedCreateVideoFileError(); + video_recorder.reset(); + video_recorder.release(); + video_recorder = null; + video_recorder_is_paused = false; + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + this.reconnectCamera(true); + } + catch(RuntimeException e) { + // needed for emulator at least - although MediaRecorder not meant to work with emulator, it's good to fail gracefully + if( MyDebug.LOG ) + Log.e(TAG, "runtime exception starting video recorder"); + e.printStackTrace(); + if( told_app_starting ) { + applicationInterface.stoppingVideo(); + } + failedToStartVideoRecorder(profile); + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.e(TAG, "camera exception starting video recorder"); + e.printStackTrace(); + if( told_app_starting ) { + applicationInterface.stoppingVideo(); + } + failedToStartVideoRecorder(profile); + } + catch(NoFreeStorageException e) { + if( MyDebug.LOG ) + Log.e(TAG, "nofreestorageexception starting video recorder"); + e.printStackTrace(); + if( told_app_starting ) { + applicationInterface.stoppingVideo(); + } + video_recorder.reset(); + video_recorder.release(); + video_recorder = null; + video_recorder_is_paused = false; + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + this.reconnectCamera(true); + this.showToast(null, R.string.video_no_free_space); + } + + { + // handle restarts + if( remaining_restart_video == 0 && !max_filesize_restart ) { + remaining_restart_video = applicationInterface.getVideoRestartTimesPref(); + if( MyDebug.LOG ) + Log.d(TAG, "initialised remaining_restart_video to: " + remaining_restart_video); + } + + if( delay_time_video == 0 && !max_filesize_restart ) { + delay_time_video = applicationInterface.getVideoDelayTimesPref(); + if( MyDebug.LOG ) + Log.d(TAG, "initialised delay_time_video to: " + delay_time_video); + } + + if( applicationInterface.getVideoFlashPref() && supportsFlash() ) { + class FlashVideoTimerTask extends TimerTask { + public void run() { + if( MyDebug.LOG ) + Log.e(TAG, "FlashVideoTimerTask"); + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + // but still need to check that the camera hasn't closed or the task halted, since TimerTask.run() started + if( camera_controller != null && flashVideoTimerTask != null ) + flashVideo(); + else { + if( MyDebug.LOG ) + Log.d(TAG, "flashVideoTimerTask: don't flash video, as already cancelled"); + } + } + }); + } + } + flashVideoTimer.schedule(flashVideoTimerTask = new FlashVideoTimerTask(), 0, 1000); + } + + if( applicationInterface.getVideoLowPowerCheckPref() ) { + /* When a device shuts down due to power off, the application will receive shutdown signals, and normally the video + * should stop and be valid. However it can happen that the video ends up corrupted (I've had people telling me this + * can happen; Googling finds plenty of stories of this happening on Android devices). I think the issue is that for + * very large videos, a lot of time is spent processing during the MediaRecorder.stop() call - if that doesn't complete + * by the time the device switches off, the video may be corrupt. + * So we add an extra safety net - devices typically turn off abou 1%, but we stop video at 3% to be safe. The user + * can try recording more videos after that if the want, but this reduces the risk that really long videos are entirely + * lost. + */ + class BatteryCheckVideoTimerTask extends TimerTask { + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "BatteryCheckVideoTimerTask"); + + // only check periodically - unclear if checking is costly in any way + // note that it's fine to call registerReceiver repeatedly - we pass a null receiver, so this is fine as a "one shot" use + Intent batteryStatus = getContext().registerReceiver(null, battery_ifilter); + int battery_level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int battery_scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + double battery_frac = battery_level/(double)battery_scale; + if( MyDebug.LOG ) + Log.d(TAG, "batteryCheckVideoTimerTask: battery level at: " + battery_frac); + + if( battery_frac <= 0.03 ) { + if( MyDebug.LOG ) + Log.d(TAG, "batteryCheckVideoTimerTask: battery at critical level, switching off video"); + Activity activity = (Activity)Preview.this.getContext(); + activity.runOnUiThread(new Runnable() { + public void run() { + // we run on main thread to avoid problem of camera closing at the same time + // but still need to check that the camera hasn't closed or the task halted, since TimerTask.run() started + if( camera_controller != null && batteryCheckVideoTimerTask != null ) { + stopVideo(false); + String toast = getContext().getResources().getString(R.string.video_power_critical); + showToast(null, toast); // show the toast afterwards, as we're hogging the UI thread here, and media recorder takes time to stop + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "batteryCheckVideoTimerTask: don't stop video, as already cancelled"); + } + } + }); + } + } + } + final long battery_check_interval_ms = 60 * 1000; + // Since we only first check after battery_check_interval_ms, this means users will get some video recorded even if the battery is already too low. + // But this is fine, as typically short videos won't be corrupted if the device shuts off, and good to allow users to try to record a bit more if they want. + batteryCheckVideoTimer.schedule(batteryCheckVideoTimerTask = new BatteryCheckVideoTimerTask(), battery_check_interval_ms, battery_check_interval_ms); + } + } + } + } + + private void failedToStartVideoRecorder(CamcorderProfile profile) { + applicationInterface.onVideoRecordStartError(profile); + video_recorder.reset(); + video_recorder.release(); + video_recorder = null; + video_recorder_is_paused = false; + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + this.reconnectCamera(true); + } + + /** Pauses the video recording - or unpauses if already paused. + * This does nothing if isVideoRecording() returns false, or not on Android 7 or higher. + */ + @TargetApi(Build.VERSION_CODES.N) + public void pauseVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "pauseVideo"); + if( Build.VERSION.SDK_INT < Build.VERSION_CODES.N ) { + Log.e(TAG, "pauseVideo called but requires Android N"); + } + else if( this.isVideoRecording() ) { + if( video_recorder_is_paused ) { + if( MyDebug.LOG ) + Log.d(TAG, "resuming..."); + video_recorder.resume(); + video_recorder_is_paused = false; + video_start_time = System.currentTimeMillis(); + this.showToast(pause_video_toast, R.string.video_resume); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "pausing..."); + video_recorder.pause(); + video_recorder_is_paused = true; + long last_time = System.currentTimeMillis() - video_start_time; + video_accumulated_time += last_time; + if( MyDebug.LOG ) { + Log.d(TAG, "last_time: " + last_time); + Log.d(TAG, "video_accumulated_time is now: " + video_accumulated_time); + } + this.showToast(pause_video_toast, R.string.video_pause); + } + } + else { + Log.e(TAG, "pauseVideo called but not video recording"); + } + } + + /** Take photo. The caller should aready have set the phase to PHASE_TAKING_PHOTO. + */ + private void takePhoto(boolean skip_autofocus) { + if( MyDebug.LOG ) + Log.d(TAG, "takePhoto"); + applicationInterface.cameraInOperation(true); + String current_ui_focus_value = getCurrentFocusValue(); + if( MyDebug.LOG ) + Log.d(TAG, "current_ui_focus_value is " + current_ui_focus_value); + + if( autofocus_in_continuous_mode ) { + if( MyDebug.LOG ) + Log.d(TAG, "continuous mode where user touched to focus"); + synchronized(this) { + // as below, if an autofocus is in progress, then take photo when it's completed + if( focus_success == FOCUS_WAITING ) { + if( MyDebug.LOG ) + Log.d(TAG, "autofocus_in_continuous_mode: take photo after current focus"); + take_photo_after_autofocus = true; + camera_controller.setCaptureFollowAutofocusHint(true); + } + else { + // when autofocus_in_continuous_mode==true, it means the user recently touched to focus in continuous focus mode, so don't do another focus + if( MyDebug.LOG ) + Log.d(TAG, "autofocus_in_continuous_mode: no need to refocus"); + takePhotoWhenFocused(); + } + } + } + else if( camera_controller.focusIsContinuous() ) { + if( MyDebug.LOG ) + Log.d(TAG, "call autofocus for continuous focus mode"); + // we call via autoFocus(), to avoid risk of taking photo while the continuous focus is focusing - risk of blurred photo, also sometimes get bug in such situations where we end of repeatedly focusing + // this is the case even if skip_autofocus is true (as we still can't guarantee that continuous focusing might be occurring) + // note: if the user touches to focus in continuous mode, we camera controller may be in auto focus mode, so we should only enter this codepath if the camera_controller is in continuous focus mode + CameraController.AutoFocusCallback autoFocusCallback = new CameraController.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success) { + if( MyDebug.LOG ) + Log.d(TAG, "continuous mode autofocus complete: " + success); + takePhotoWhenFocused(); + } + }; + camera_controller.autoFocus(autoFocusCallback, true); + } + else if( skip_autofocus || this.recentlyFocused() ) { + if( MyDebug.LOG ) { + if( skip_autofocus ) { + Log.d(TAG, "skip_autofocus flag set"); + } + else { + Log.d(TAG, "recently focused successfully, so no need to refocus"); + } + } + takePhotoWhenFocused(); + } + else if( current_ui_focus_value != null && ( current_ui_focus_value.equals("focus_mode_auto") || current_ui_focus_value.equals("focus_mode_macro") ) ) { + // n.b., we check focus_value rather than camera_controller.supportsAutoFocus(), as we want to discount focus_mode_locked + synchronized(this) { + if( focus_success == FOCUS_WAITING ) { + // Needed to fix bug (on Nexus 6, old camera API): if flash was on, pointing at a dark scene, and we take photo when already autofocusing, the autofocus never returned so we got stuck! + // In general, probably a good idea to not redo a focus - just use the one that's already in progress + if( MyDebug.LOG ) + Log.d(TAG, "take photo after current focus"); + take_photo_after_autofocus = true; + camera_controller.setCaptureFollowAutofocusHint(true); + } + else { + focus_success = FOCUS_DONE; // clear focus rectangle for new refocus + CameraController.AutoFocusCallback autoFocusCallback = new CameraController.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success) { + if( MyDebug.LOG ) + Log.d(TAG, "autofocus complete: " + success); + ensureFlashCorrect(); // need to call this in case user takes picture before startup focus completes! + prepareAutoFocusPhoto(); + takePhotoWhenFocused(); + } + }; + if( MyDebug.LOG ) + Log.d(TAG, "start autofocus to take picture"); + camera_controller.autoFocus(autoFocusCallback, true); + count_cameraAutoFocus++; + } + } + } + else { + takePhotoWhenFocused(); + } + } + + /** Should be called when taking a photo immediately after an autofocus. + * This is needed for a workaround for Camera2 bug (at least on Nexus 6) where photos sometimes come out dark when using flash + * auto, when the flash fires. This happens when taking a photo in autofocus mode (including when continuous mode has + * transitioned to autofocus mode due to touching to focus). Seems to happen with scenes that have bright and dark regions, + * i.e., on verge of flash firing. + * Seems to be fixed if we have a short delay... + */ + private void prepareAutoFocusPhoto() { + if( MyDebug.LOG ) + Log.d(TAG, "prepareAutoFocusPhoto"); + if( using_android_l ) { + String flash_value = camera_controller.getFlashValue(); + // getFlashValue() may return "" if flash not supported! + if( flash_value.length() > 0 && ( flash_value.equals("flash_auto") || flash_value.equals("flash_red_eye") ) ) { + if( MyDebug.LOG ) + Log.d(TAG, "wait for a bit..."); + try { + Thread.sleep(100); + } + catch(InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + /** Take photo, assumes any autofocus has already been taken care of, and that applicationInterface.cameraInOperation(true) has + * already been called. + * Note that even if a caller wants to take a photo without focusing, you probably want to call takePhoto() with skip_autofocus + * set to true (so that things work okay in continuous picture focus mode). + */ + private void takePhotoWhenFocused() { + // should be called when auto-focused + if( MyDebug.LOG ) + Log.d(TAG, "takePhotoWhenFocused"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + /*is_taking_photo_on_timer = false; + is_taking_photo = false;*/ + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + return; + } + if( !this.has_surface ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview surface not yet available"); + /*is_taking_photo_on_timer = false; + is_taking_photo = false;*/ + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + return; + } + + final String focus_value = current_focus_index != -1 ? supported_focus_values.get(current_focus_index) : null; + if( MyDebug.LOG ) { + Log.d(TAG, "focus_value is " + focus_value); + Log.d(TAG, "focus_success is " + focus_success); + } + + if( focus_value != null && focus_value.equals("focus_mode_locked") && focus_success == FOCUS_WAITING ) { + // make sure there isn't an autofocus in progress - can happen if in locked mode we take a photo while autofocusing - see testTakePhotoLockedFocus() (although that test doesn't always properly test the bug...) + // we only cancel when in locked mode and if still focusing, as I had 2 bug reports for v1.16 that the photo was being taken out of focus; both reports said it worked fine in 1.15, and one confirmed that it was due to the cancelAutoFocus() line, and that it's now fixed with this fix + // they said this happened in every focus mode, including locked - so possible that on some devices, cancelAutoFocus() actually pulls the camera out of focus, or reverts to preview focus? + cancelAutoFocus(); + } + removePendingContinuousFocusReset(); // to avoid switching back to continuous focus mode while taking a photo - instead we'll always make sure we switch back after taking a photo + updateParametersFromLocation(); // do this now, not before, so we don't set location parameters during focus (sometimes get RuntimeException) + + focus_success = FOCUS_DONE; // clear focus rectangle if not already done + successfully_focused = false; // so next photo taken will require an autofocus + if( MyDebug.LOG ) + Log.d(TAG, "remaining_burst_photos: " + remaining_burst_photos); + + CameraController.PictureCallback pictureCallback = new CameraController.PictureCallback() { + private boolean success = false; // whether jpeg callback succeeded + private boolean has_date = false; + private Date current_date = null; + + public void onStarted() { + if( MyDebug.LOG ) + Log.d(TAG, "onStarted"); + applicationInterface.onCaptureStarted(); + } + + public void onCompleted() { + if( MyDebug.LOG ) + Log.d(TAG, "onCompleted"); + applicationInterface.onPictureCompleted(); + if( !using_android_l ) { + is_preview_started = false; // preview automatically stopped due to taking photo on original Camera API + } + phase = PHASE_NORMAL; // need to set this even if remaining burst photos, so we can restart the preview + if( remaining_burst_photos == -1 || remaining_burst_photos > 0 ) { + if( !is_preview_started ) { + // we need to restart the preview; and we do this in the callback, as we need to restart after saving the image + // (otherwise this can fail, at least on Nexus 7) + if( MyDebug.LOG ) + Log.d(TAG, "burst mode photos remaining: onPictureTaken about to start preview: " + remaining_burst_photos); + startCameraPreview(); + if( MyDebug.LOG ) + Log.d(TAG, "burst mode photos remaining: onPictureTaken started preview: " + remaining_burst_photos); + } + } + else { + phase = PHASE_NORMAL; + boolean pause_preview = applicationInterface.getPausePreviewPref(); + if( MyDebug.LOG ) + Log.d(TAG, "pause_preview? " + pause_preview); + if( pause_preview && success ) { + if( is_preview_started ) { + // need to manually stop preview on Android L Camera2 + camera_controller.stopPreview(); + is_preview_started = false; + } + setPreviewPaused(true); + } + else { + if( !is_preview_started ) { + // we need to restart the preview; and we do this in the callback, as we need to restart after saving the image + // (otherwise this can fail, at least on Nexus 7) + startCameraPreview(); + } + applicationInterface.cameraInOperation(false); + if( MyDebug.LOG ) + Log.d(TAG, "onPictureTaken started preview"); + } + } + continuousFocusReset(); // in case we took a photo after user had touched to focus (causing us to switch from continuous to autofocus mode) + if( camera_controller != null && focus_value != null && ( focus_value.equals("focus_mode_continuous_picture") || focus_value.equals("focus_mode_continuous_video") ) ) { + if( MyDebug.LOG ) + Log.d(TAG, "cancelAutoFocus to restart continuous focusing"); + camera_controller.cancelAutoFocus(); // needed to restart continuous focusing + } + + if( MyDebug.LOG ) + Log.d(TAG, "do we need to take another photo? remaining_burst_photos: " + remaining_burst_photos); + if( remaining_burst_photos == -1 || remaining_burst_photos > 0 ) { + if( remaining_burst_photos > 0 ) + remaining_burst_photos--; + + long timer_delay = applicationInterface.getRepeatIntervalPref(); + if( timer_delay == 0 ) { + // we set skip_autofocus to go straight to taking a photo rather than refocusing, for speed + // need to manually set the phase + phase = PHASE_TAKING_PHOTO; + takePhoto(true); + } + else { + takePictureOnTimer(timer_delay, true); + } + } + } + + /** Ensures we get the same date for both JPEG and RAW; and that we set the date ASAP so that it corresponds to actual + * photo time. + */ + private void initDate() { + if( !has_date ) { + has_date = true; + current_date = new Date(); + if( MyDebug.LOG ) + Log.d(TAG, "picture taken on date: " + current_date); + } + } + + public void onPictureTaken(byte[] data) { + if( MyDebug.LOG ) + Log.d(TAG, "onPictureTaken"); + // n.b., this is automatically run in a different thread + initDate(); + if( !applicationInterface.onPictureTaken(data, current_date) ) { + if( MyDebug.LOG ) + Log.e(TAG, "applicationInterface.onPictureTaken failed"); + success = false; + } + else { + success = true; + } + } + + public void onRawPictureTaken(DngCreator dngCreator, Image image) { + if( MyDebug.LOG ) + Log.d(TAG, "onRawPictureTaken"); + initDate(); + if( !applicationInterface.onRawPictureTaken(dngCreator, image, current_date) ) { + if( MyDebug.LOG ) + Log.e(TAG, "applicationInterface.onRawPictureTaken failed"); + } + } + + public void onBurstPictureTaken(List images) { + if( MyDebug.LOG ) + Log.d(TAG, "onBurstPictureTaken"); + // n.b., this is automatically run in a different thread + initDate(); + + success = true; + if( !applicationInterface.onBurstPictureTaken(images, current_date) ) { + if( MyDebug.LOG ) + Log.e(TAG, "applicationInterface.onBurstPictureTaken failed"); + success = false; + } + } + + public void onFrontScreenTurnOn() { + if( MyDebug.LOG ) + Log.d(TAG, "onFrontScreenTurnOn"); + applicationInterface.turnFrontScreenFlashOn(); + } + }; + CameraController.ErrorCallback errorCallback = new CameraController.ErrorCallback() { + public void onError() { + if( MyDebug.LOG ) + Log.e(TAG, "error from takePicture"); + count_cameraTakePicture--; // cancel out the increment from after the takePicture() call + applicationInterface.onPhotoError(); + phase = PHASE_NORMAL; + startCameraPreview(); + applicationInterface.cameraInOperation(false); + } + }; + { + camera_controller.setRotation(getImageVideoRotation()); + + boolean enable_sound = applicationInterface.getShutterSoundPref(); + if( MyDebug.LOG ) + Log.d(TAG, "enable_sound? " + enable_sound); + camera_controller.enableShutterSound(enable_sound); + if( using_android_l ) { + boolean use_camera2_fast_burst = applicationInterface.useCamera2FastBurst(); + if( MyDebug.LOG ) + Log.d(TAG, "use_camera2_fast_burst? " + use_camera2_fast_burst); + camera_controller.setUseExpoFastBurst( use_camera2_fast_burst ); + } + if( MyDebug.LOG ) + Log.d(TAG, "about to call takePicture"); + camera_controller.takePicture(pictureCallback, errorCallback); + count_cameraTakePicture++; + } + if( MyDebug.LOG ) + Log.d(TAG, "takePhotoWhenFocused exit"); + } + + /*void clickedShare() { + if( MyDebug.LOG ) + Log.d(TAG, "clickedShare"); + //if( is_preview_paused ) { + if( this.phase == PHASE_PREVIEW_PAUSED ) { + if( preview_image_name != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "Share: " + preview_image_name); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("image/jpeg"); + intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + preview_image_name)); + Activity activity = (Activity)this.getContext(); + activity.startActivity(Intent.createChooser(intent, "Photo")); + } + startCameraPreview(); + tryAutoFocus(false, false); + } + } + + void clickedTrash() { + if( MyDebug.LOG ) + Log.d(TAG, "clickedTrash"); + //if( is_preview_paused ) { + if( this.phase == PHASE_PREVIEW_PAUSED ) { + if( preview_image_name != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "Delete: " + preview_image_name); + File file = new File(preview_image_name); + if( !file.delete() ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to delete " + preview_image_name); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "successfully deleted " + preview_image_name); + showToast(null, R.string.photo_deleted); + applicationInterface.broadcastFile(file, false, false); + } + } + startCameraPreview(); + tryAutoFocus(false, false); + } + }*/ + + public void requestAutoFocus() { + if( MyDebug.LOG ) + Log.d(TAG, "requestAutoFocus"); + cancelAutoFocus(); + tryAutoFocus(false, true); + } + + private void tryAutoFocus(final boolean startup, final boolean manual) { + // manual: whether user has requested autofocus (e.g., by touching screen, or volume focus, or hardware focus button) + // consider whether you want to call requestAutoFocus() instead (which properly cancels any in-progress auto-focus first) + if( MyDebug.LOG ) { + Log.d(TAG, "tryAutoFocus"); + Log.d(TAG, "startup? " + startup); + Log.d(TAG, "manual? " + manual); + } + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + } + else if( !this.has_surface ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview surface not yet available"); + } + else if( !this.is_preview_started ) { + if( MyDebug.LOG ) + Log.d(TAG, "preview not yet started"); + } + //else if( is_taking_photo ) { + else if( !(manual && this.is_video) && this.isTakingPhotoOrOnTimer() ) { + // if taking a video, we allow manual autofocuses + // autofocus may cause problem if there is a video corruption problem, see testTakeVideoBitrate() on Nexus 7 at 30Mbs or 50Mbs, where the startup autofocus would cause a problem here + if( MyDebug.LOG ) + Log.d(TAG, "currently taking a photo"); + } + else { + if( manual ) { + // remove any previous request to switch back to continuous + removePendingContinuousFocusReset(); + } + if( manual && !is_video && camera_controller.focusIsContinuous() && supportedFocusValue("focus_mode_auto") ) { + if( MyDebug.LOG ) + Log.d(TAG, "switch from continuous to autofocus mode for touch focus"); + camera_controller.setFocusValue("focus_mode_auto"); // switch to autofocus + autofocus_in_continuous_mode = true; + // we switch back to continuous via a new reset_continuous_focus_runnable in autoFocusCompleted() + } + // it's only worth doing autofocus when autofocus has an effect (i.e., auto or macro mode) + // but also for continuous focus mode, triggering an autofocus is still important to fire flash when touching the screen + if( camera_controller.supportsAutoFocus() ) { + if( MyDebug.LOG ) + Log.d(TAG, "try to start autofocus"); + if( !using_android_l ) { + set_flash_value_after_autofocus = ""; + String old_flash_value = camera_controller.getFlashValue(); + // getFlashValue() may return "" if flash not supported! + if( startup && old_flash_value.length() > 0 && !old_flash_value.equals("flash_off") && !old_flash_value.equals("flash_torch") ) { + set_flash_value_after_autofocus = old_flash_value; + camera_controller.setFlashValue("flash_off"); + } + if( MyDebug.LOG ) + Log.d(TAG, "set_flash_value_after_autofocus is now: " + set_flash_value_after_autofocus); + } + CameraController.AutoFocusCallback autoFocusCallback = new CameraController.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success) { + if( MyDebug.LOG ) + Log.d(TAG, "autofocus complete: " + success); + autoFocusCompleted(manual, success, false); + } + }; + + this.focus_success = FOCUS_WAITING; + if( MyDebug.LOG ) + Log.d(TAG, "set focus_success to " + focus_success); + this.focus_complete_time = -1; + this.successfully_focused = false; + camera_controller.autoFocus(autoFocusCallback, false); + count_cameraAutoFocus++; + this.focus_started_time = System.currentTimeMillis(); + if( MyDebug.LOG ) + Log.d(TAG, "autofocus started, count now: " + count_cameraAutoFocus); + } + else if( has_focus_area ) { + // do this so we get the focus box, for focus modes that support focus area, but don't support autofocus + focus_success = FOCUS_SUCCESS; + focus_complete_time = System.currentTimeMillis(); + // n.b., don't set focus_started_time as that may be used for application to show autofocus animation + } + } + } + + /** If the user touches the screen in continuous focus mode, we switch the camera_controller to autofocus mode. + * After the autofocus completes, we set a reset_continuous_focus_runnable to switch back to the camera_controller + * back to continuous focus after a short delay. + * This function removes any pending reset_continuous_focus_runnable. + */ + private void removePendingContinuousFocusReset() { + if( MyDebug.LOG ) + Log.d(TAG, "removePendingContinuousFocusReset"); + if( reset_continuous_focus_runnable != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove pending reset_continuous_focus_runnable"); + reset_continuous_focus_handler.removeCallbacks(reset_continuous_focus_runnable); + reset_continuous_focus_runnable = null; + } + } + + /** If the user touches the screen in continuous focus mode, we switch the camera_controller to autofocus mode. + * This function is called to see if we should switch from autofocus mode back to continuous focus mode. + * If this isn't required, calling this function does nothing. + */ + private void continuousFocusReset() { + if( MyDebug.LOG ) + Log.d(TAG, "switch back to continuous focus after autofocus?"); + if( camera_controller != null && autofocus_in_continuous_mode ) { + autofocus_in_continuous_mode = false; + // check again + String current_ui_focus_value = getCurrentFocusValue(); + if( current_ui_focus_value != null && !camera_controller.getFocusValue().equals(current_ui_focus_value) && camera_controller.getFocusValue().equals("focus_mode_auto") ) { + camera_controller.cancelAutoFocus(); + camera_controller.setFocusValue(current_ui_focus_value); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no need to switch back to continuous focus after autofocus, mode already changed"); + } + } + } + + private void cancelAutoFocus() { + if( MyDebug.LOG ) + Log.d(TAG, "cancelAutoFocus"); + if( camera_controller != null ) { + camera_controller.cancelAutoFocus(); + autoFocusCompleted(false, false, true); + } + } + + private void ensureFlashCorrect() { + // ensures flash is in correct mode, in case where we had to turn flash temporarily off for startup autofocus + if( set_flash_value_after_autofocus.length() > 0 && camera_controller != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "set flash back to: " + set_flash_value_after_autofocus); + camera_controller.setFlashValue(set_flash_value_after_autofocus); + set_flash_value_after_autofocus = ""; + } + } + + private void autoFocusCompleted(boolean manual, boolean success, boolean cancelled) { + if( MyDebug.LOG ) { + Log.d(TAG, "autoFocusCompleted"); + Log.d(TAG, " manual? " + manual); + Log.d(TAG, " success? " + success); + Log.d(TAG, " cancelled? " + cancelled); + } + if( cancelled ) { + focus_success = FOCUS_DONE; + } + else { + focus_success = success ? FOCUS_SUCCESS : FOCUS_FAILED; + focus_complete_time = System.currentTimeMillis(); + } + if( manual && !cancelled && ( success || applicationInterface.isTestAlwaysFocus() ) ) { + successfully_focused = true; + successfully_focused_time = focus_complete_time; + } + if( manual && camera_controller != null && autofocus_in_continuous_mode ) { + String current_ui_focus_value = getCurrentFocusValue(); + if( MyDebug.LOG ) + Log.d(TAG, "current_ui_focus_value: " + current_ui_focus_value); + if( current_ui_focus_value != null && !camera_controller.getFocusValue().equals(current_ui_focus_value) && camera_controller.getFocusValue().equals("focus_mode_auto") ) { + reset_continuous_focus_runnable = new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "reset_continuous_focus_runnable running..."); + reset_continuous_focus_runnable = null; + continuousFocusReset(); + } + }; + reset_continuous_focus_handler.postDelayed(reset_continuous_focus_runnable, 3000); + } + } + ensureFlashCorrect(); + if( this.using_face_detection && !cancelled ) { + // On some devices such as mtk6589, face detection does not resume as written in documentation so we have + // to cancelfocus when focus is finished + if( camera_controller != null ) { + camera_controller.cancelAutoFocus(); + } + } + synchronized(this) { + if( take_photo_after_autofocus ) { + if( MyDebug.LOG ) + Log.d(TAG, "take_photo_after_autofocus is set"); + take_photo_after_autofocus = false; + prepareAutoFocusPhoto(); + takePhotoWhenFocused(); + } + } + if( MyDebug.LOG ) + Log.d(TAG, "autoFocusCompleted exit"); + } + + public void startCameraPreview() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "startCameraPreview"); + debug_time = System.currentTimeMillis(); + } + //if( camera != null && !is_taking_photo && !is_preview_started ) { + if( camera_controller != null && !this.isTakingPhotoOrOnTimer() && !is_preview_started ) { + if( MyDebug.LOG ) + Log.d(TAG, "starting the camera preview"); + { + if( MyDebug.LOG ) + Log.d(TAG, "setRecordingHint: " + is_video); + camera_controller.setRecordingHint(this.is_video); + } + setPreviewFps(); + try { + camera_controller.startPreview(); + count_cameraStartPreview++; + } + catch(CameraControllerException e) { + if( MyDebug.LOG ) + Log.d(TAG, "CameraControllerException trying to startPreview"); + e.printStackTrace(); + applicationInterface.onFailedStartPreview(); + return; + } + this.is_preview_started = true; + if( MyDebug.LOG ) { + Log.d(TAG, "startCameraPreview: time after starting camera preview: " + (System.currentTimeMillis() - debug_time)); + } + if( this.using_face_detection ) { + if( MyDebug.LOG ) + Log.d(TAG, "start face detection"); + camera_controller.startFaceDetection(); + faces_detected = null; + } + } + this.setPreviewPaused(false); + this.setupContinuousFocusMove(); + if( MyDebug.LOG ) { + Log.d(TAG, "startCameraPreview: total time for startCameraPreview: " + (System.currentTimeMillis() - debug_time)); + } + } + + private void setPreviewPaused(boolean paused) { + if( MyDebug.LOG ) + Log.d(TAG, "setPreviewPaused: " + paused); + applicationInterface.hasPausedPreview(paused); + if( paused ) { + this.phase = PHASE_PREVIEW_PAUSED; + // shouldn't call applicationInterface.cameraInOperation(true), as should already have done when we started to take a photo (or above when exiting immersive mode) + } + else { + this.phase = PHASE_NORMAL; + applicationInterface.cameraInOperation(false); + } + } + + public void onAccelerometerSensorChanged(SensorEvent event) { + /*if( MyDebug.LOG ) + Log.d(TAG, "onAccelerometerSensorChanged: " + event.values[0] + ", " + event.values[1] + ", " + event.values[2]);*/ + + this.has_gravity = true; + for(int i=0;i<3;i++) { + //this.gravity[i] = event.values[i]; + this.gravity[i] = sensor_alpha * this.gravity[i] + (1.0f-sensor_alpha) * event.values[i]; + } + calculateGeoDirection(); + + double x = gravity[0]; + double y = gravity[1]; + double z = gravity[2]; + double mag = Math.sqrt(x*x + y*y + z*z); + /*if( MyDebug.LOG ) + Log.d(TAG, "xyz: " + x + ", " + y + ", " + z);*/ + + this.has_pitch_angle = false; + if( mag > 1.0e-8 ) { + this.has_pitch_angle = true; + this.pitch_angle = Math.asin(- z / mag) * 180.0 / Math.PI; + /*if( MyDebug.LOG ) + Log.d(TAG, "pitch: " + pitch_angle);*/ + + if( !is_test && Math.abs(pitch_angle) > 70.0 ) { + // level angle becomes unstable when device is near vertical + // note that if is_test, we always set the level angle - since the device typically lies face down when running tests... + this.has_level_angle = false; + } + else { + this.has_level_angle = true; + this.natural_level_angle = Math.atan2(-x, y) * 180.0 / Math.PI; + if( this.natural_level_angle < -0.0 ) { + this.natural_level_angle += 360.0; + } + + updateLevelAngles(); + } + } + else { + Log.e(TAG, "accel sensor has zero mag: " + mag); + this.has_level_angle = false; + } + + } + + /** This method should be called when the natural level angle, or the calibration angle, has been updated, to update the other level angle variables. + * + */ + public void updateLevelAngles() { + if( has_level_angle ) { + this.level_angle = this.natural_level_angle; + double calibrated_level_angle = applicationInterface.getCalibratedLevelAngle(); + this.level_angle -= calibrated_level_angle; + this.orig_level_angle = this.level_angle; + this.level_angle -= (float) this.current_orientation; + if( this.level_angle < -180.0 ) { + this.level_angle += 360.0; + } + else if( this.level_angle > 180.0 ) { + this.level_angle -= 360.0; + } + /*if( MyDebug.LOG ) + Log.d(TAG, "level_angle is now: " + level_angle);*/ + } + } + + public boolean hasLevelAngle() { + return this.has_level_angle; + } + + public double getLevelAngleUncalibrated() { + return this.natural_level_angle - this.current_orientation; + } + + public double getLevelAngle() { + return this.level_angle; + } + + public double getOrigLevelAngle() { + return this.orig_level_angle; + } + + public boolean hasPitchAngle() { + return this.has_pitch_angle; + } + + public double getPitchAngle() { + return this.pitch_angle; + } + + public void onMagneticSensorChanged(SensorEvent event) { + this.has_geomagnetic = true; + for(int i=0;i<3;i++) { + //this.geomagnetic[i] = event.values[i]; + this.geomagnetic[i] = sensor_alpha * this.geomagnetic[i] + (1.0f-sensor_alpha) * event.values[i]; + } + calculateGeoDirection(); + } + + private void calculateGeoDirection() { + if( !this.has_gravity || !this.has_geomagnetic ) { + return; + } + if( !SensorManager.getRotationMatrix(this.deviceRotation, this.deviceInclination, this.gravity, this.geomagnetic) ) { + return; + } + SensorManager.remapCoordinateSystem(this.deviceRotation, SensorManager.AXIS_X, SensorManager.AXIS_Z, this.cameraRotation); + boolean has_old_geo_direction = has_geo_direction; + this.has_geo_direction = true; + //SensorManager.getOrientation(cameraRotation, geo_direction); + SensorManager.getOrientation(cameraRotation, new_geo_direction); + /*if( MyDebug.LOG ) { + Log.d(TAG, "###"); + Log.d(TAG, "old geo_direction: " + (geo_direction[0]*180/Math.PI) + ", " + (geo_direction[1]*180/Math.PI) + ", " + (geo_direction[2]*180/Math.PI)); + }*/ + for(int i=0;i<3;i++) { + float old_compass = (float)Math.toDegrees(geo_direction[i]); + float new_compass = (float)Math.toDegrees(new_geo_direction[i]); + if( has_old_geo_direction ) { + float smoothFactorCompass = 0.1f; + float smoothThresholdCompass = 10.0f; + old_compass = lowPassFilter(old_compass, new_compass, smoothFactorCompass, smoothThresholdCompass); + } + else { + old_compass = new_compass; + } + geo_direction[i] = (float)Math.toRadians(old_compass); + } + /*if( MyDebug.LOG ) { + Log.d(TAG, "new_geo_direction: " + (new_geo_direction[0]*180/Math.PI) + ", " + (new_geo_direction[1]*180/Math.PI) + ", " + (new_geo_direction[2]*180/Math.PI)); + Log.d(TAG, "geo_direction: " + (geo_direction[0]*180/Math.PI) + ", " + (geo_direction[1]*180/Math.PI) + ", " + (geo_direction[2]*180/Math.PI)); + }*/ + } + + /** Low pass filter, for angles. + * @param old_value Old value in degrees. + * @param new_value New value in degrees. + */ + private float lowPassFilter(float old_value, float new_value, float smoothFactorCompass, float smoothThresholdCompass) { + // see http://stackoverflow.com/questions/4699417/android-compass-orientation-on-unreliable-low-pass-filter + // https://www.built.io/blog/applying-low-pass-filter-to-android-sensor-s-readings + // http://stackoverflow.com/questions/27846604/how-to-get-smooth-orientation-data-in-android + float diff = Math.abs(new_value - old_value); + /*if( MyDebug.LOG ) + Log.d(TAG, "diff: " + diff);*/ + if( diff < 180 ) { + if( diff > smoothThresholdCompass ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "jump to new compass");*/ + old_value = new_value; + } + else { + old_value = old_value + smoothFactorCompass * (new_value - old_value); + } + } + else { + if( 360.0 - diff > smoothThresholdCompass ) { + /*if( MyDebug.LOG ) + Log.d(TAG, "jump to new compass");*/ + old_value = new_value; + } + else { + if( old_value > new_value ) { + old_value = (old_value + smoothFactorCompass * ((360 + new_value - old_value) % 360) + 360) % 360; + } + else { + old_value = (old_value - smoothFactorCompass * ((360 - new_value + old_value) % 360) + 360) % 360; + } + } + } + return old_value; + } + + public boolean hasGeoDirection() { + return has_geo_direction; + } + + public double getGeoDirection() { + return geo_direction[0]; + } + + public boolean supportsFaceDetection() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsFaceDetection"); + return supports_face_detection; + } + + public boolean supportsVideoStabilization() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsVideoStabilization"); + return supports_video_stabilization; + } + + public boolean canDisableShutterSound() { + if( MyDebug.LOG ) + Log.d(TAG, "canDisableShutterSound"); + return can_disable_shutter_sound; + } + + public List getSupportedColorEffects() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedColorEffects"); + return this.color_effects; + } + + public List getSupportedSceneModes() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedSceneModes"); + return this.scene_modes; + } + + public List getSupportedWhiteBalances() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedWhiteBalances"); + return this.white_balances; + } + + public String getISOKey() { + if( MyDebug.LOG ) + Log.d(TAG, "getISOKey"); + return camera_controller == null ? "" : camera_controller.getISOKey(); + } + + /** Returns whether a range of manual ISO values can be set. If this returns true, use + * getMinimumISO() and getMaximumISO() to return the valid range of values. If this returns + * false, getSupportedISOs() to find allowed ISO values. + */ + public boolean supportsISORange() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsISORange"); + return this.supports_iso_range; + } + + /** If supportsISORange() returns false, use this method to return a list of supported ISO values: + * - If this is null, then manual ISO isn't supported. + * - If non-null, this will include "auto" to indicate auto-ISO, and one or more numerical ISO + * values. + * If supportsISORange() returns true, then this method should not be used (and it will return + * null). Instead use getMinimumISO() and getMaximumISO(). + */ + public List getSupportedISOs() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedISOs"); + return this.isos; + } + + /** Returns minimum ISO value. Only relevant if supportsISORange() returns true. + */ + public int getMinimumISO() { + if( MyDebug.LOG ) + Log.d(TAG, "getMinimumISO"); + return this.min_iso; + } + + /** Returns maximum ISO value. Only relevant if supportsISORange() returns true. + */ + public int getMaximumISO() { + if( MyDebug.LOG ) + Log.d(TAG, "getMaximumISO"); + return this.max_iso; + } + + public float getMinimumFocusDistance() { + return this.minimum_focus_distance; + } + + public boolean supportsExposureTime() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsExposureTime"); + return this.supports_exposure_time; + } + + public long getMinimumExposureTime() { + if( MyDebug.LOG ) + Log.d(TAG, "getMinimumExposureTime"); + return this.min_exposure_time; + } + + public long getMaximumExposureTime() { + if( MyDebug.LOG ) + Log.d(TAG, "getMaximumExposureTime"); + return this.max_exposure_time; + } + + public boolean supportsExposures() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsExposures"); + return this.exposures != null; + } + + public int getMinimumExposure() { + if( MyDebug.LOG ) + Log.d(TAG, "getMinimumExposure"); + return this.min_exposure; + } + + public int getMaximumExposure() { + if( MyDebug.LOG ) + Log.d(TAG, "getMaximumExposure"); + return this.max_exposure; + } + + public int getCurrentExposure() { + if( MyDebug.LOG ) + Log.d(TAG, "getCurrentExposure"); + if( camera_controller == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return 0; + } + return camera_controller.getExposureCompensation(); + } + + /*List getSupportedExposures() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedExposures"); + return this.exposures; + }*/ + + public boolean supportsExpoBracketing() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsExpoBracketing"); + return this.supports_expo_bracketing; + } + + public boolean supportsRaw() { + if( MyDebug.LOG ) + Log.d(TAG, "supportsRaw"); + return this.supports_raw; + } + + public float getViewAngleX() { + return this.view_angle_x; + } + + public float getViewAngleY() { + return this.view_angle_y; + } + + public List getSupportedPreviewSizes() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedPreviewSizes"); + return this.supported_preview_sizes; + } + + public CameraController.Size getCurrentPreviewSize() { + return new CameraController.Size(preview_w, preview_h); + } + + public List getSupportedPictureSizes() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedPictureSizes"); + return this.sizes; + } + + public int getCurrentPictureSizeIndex() { + if( MyDebug.LOG ) + Log.d(TAG, "getCurrentPictureSizeIndex"); + return this.current_size_index; + } + + public CameraController.Size getCurrentPictureSize() { + if( current_size_index == -1 || sizes == null ) + return null; + return sizes.get(current_size_index); + } + + public VideoQualityHandler getVideoQualityHander() { + return this.video_quality_handler; + } + + public List getSupportedFlashValues() { + return supported_flash_values; + } + + public List getSupportedFocusValues() { + return supported_focus_values; + } + + public int getCameraId() { + if( camera_controller == null ) + return 0; + return camera_controller.getCameraId(); + } + + public String getCameraAPI() { + if( camera_controller == null ) + return "None"; + return camera_controller.getAPI(); + } + + public void onResume() { + if( MyDebug.LOG ) + Log.d(TAG, "onResume"); + this.app_is_paused = false; + this.openCamera(); + } + + public void onPause() { + if( MyDebug.LOG ) + Log.d(TAG, "onPause"); + this.app_is_paused = true; + this.closeCamera(); + } + + /*void updateUIPlacement() { + // we cache the preference_ui_placement to save having to check it in the draw() method + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + String ui_placement = sharedPreferences.getString(MainActivity.getUIPlacementPreferenceKey(), "ui_right"); + this.ui_placement_right = ui_placement.equals("ui_right"); + }*/ + + public void onSaveInstanceState(Bundle state) { + if( MyDebug.LOG ) + Log.d(TAG, "onSaveInstanceState"); + } + + public void showToast(final ToastBoxer clear_toast, final int message_id) { + showToast(clear_toast, getResources().getString(message_id)); + } + + public void showToast(final ToastBoxer clear_toast, final String message) { + showToast(clear_toast, message, 32); + } + + private void showToast(final ToastBoxer clear_toast, final String message, final int offset_y_dp) { + if( !applicationInterface.getShowToastsPref() ) { + return; + } + + class RotatedTextView extends View { + private String [] lines; + private final Paint paint = new Paint(); + private final Rect bounds = new Rect(); + private final Rect sub_bounds = new Rect(); + private final RectF rect = new RectF(); + + public RotatedTextView(String text, Context context) { + super(context); + + this.lines = text.split("\n"); + } + + void setText(String text) { + this.lines = text.split("\n"); + } + + @Override + protected void onDraw(Canvas canvas) { + final float scale = Preview.this.getResources().getDisplayMetrics().density; + paint.setTextSize(14 * scale + 0.5f); // convert dps to pixels + paint.setShadowLayer(1, 0, 1, Color.BLACK); + //paint.getTextBounds(text, 0, text.length(), bounds); + boolean first_line = true; + for(String line : lines) { + paint.getTextBounds(line, 0, line.length(), sub_bounds); + /*if( MyDebug.LOG ) { + Log.d(TAG, "line: " + line + " sub_bounds: " + sub_bounds); + }*/ + if( first_line ) { + bounds.set(sub_bounds); + first_line = false; + } + else { + bounds.top = Math.min(sub_bounds.top, bounds.top); + bounds.bottom = Math.max(sub_bounds.bottom, bounds.bottom); + bounds.left = Math.min(sub_bounds.left, bounds.left); + bounds.right = Math.max(sub_bounds.right, bounds.right); + } + } + // above we've worked out the maximum bounds of each line - this is useful for left/right, but for the top/bottom + // we would rather use a consistent height no matter what the text is (otherwise we have the problem of varying + // gap between lines, depending on what the characters are). + final String reference_text = "Ap"; + paint.getTextBounds(reference_text, 0, reference_text.length(), sub_bounds); + bounds.top = sub_bounds.top; + bounds.bottom = sub_bounds.bottom; + /*if( MyDebug.LOG ) { + Log.d(TAG, "bounds: " + bounds); + }*/ + int height = bounds.bottom - bounds.top; // height of each line + bounds.bottom += ((lines.length-1) * height)/2; + bounds.top -= ((lines.length-1) * height)/2; + final int padding = (int) (14 * scale + 0.5f); // padding for the shaded rectangle; convert dps to pixels + final int offset_y = (int) (offset_y_dp * scale + 0.5f); // convert dps to pixels + canvas.save(); + canvas.rotate(ui_rotation, canvas.getWidth()/2.0f, canvas.getHeight()/2.0f); + + rect.left = canvas.getWidth()/2 - bounds.width()/2 + bounds.left - padding; + rect.top = canvas.getHeight()/2 + bounds.top - padding + offset_y; + rect.right = canvas.getWidth()/2 - bounds.width()/2 + bounds.right + padding; + rect.bottom = canvas.getHeight()/2 + bounds.bottom + padding + offset_y; + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.rgb(50, 50, 50)); + //canvas.drawRect(rect, paint); + final float radius = (24 * scale + 0.5f); // convert dps to pixels + canvas.drawRoundRect(rect, radius, radius, paint); + + paint.setColor(Color.WHITE); + int ypos = canvas.getHeight()/2 + offset_y - ((lines.length-1) * height)/2; + for(String line : lines) { + canvas.drawText(line, canvas.getWidth()/2 - bounds.width()/2, ypos, paint); + ypos += height; + } + canvas.restore(); + } + } + + if( MyDebug.LOG ) + Log.d(TAG, "showToast: " + message); + final Activity activity = (Activity)this.getContext(); + // We get a crash on emulator at least if Toast constructor isn't run on main thread (e.g., the toast for taking a photo when on timer). + // Also see http://stackoverflow.com/questions/13267239/toast-from-a-non-ui-thread + activity.runOnUiThread(new Runnable() { + public void run() { + /*if( clear_toast != null && clear_toast.toast != null ) + clear_toast.toast.cancel(); + + Toast toast = new Toast(activity); + if( clear_toast != null ) + clear_toast.toast = toast;*/ + // This method is better, as otherwise a previous toast (with different or no clear_toast) never seems to clear if we repeatedly issue new toasts - this doesn't happen if we reuse existing toasts if possible + // However should only do this if the previous toast was the most recent toast (to avoid messing up ordering) + Toast toast; + if( clear_toast != null && clear_toast.toast != null && clear_toast.toast == last_toast ) { + if( MyDebug.LOG ) + Log.d(TAG, "reuse last toast: " + last_toast); + toast = clear_toast.toast; + // for performance, important to reuse the same view, instead of creating a new one (otherwise we get jerky preview update e.g. for changing manual focus slider) + RotatedTextView view = (RotatedTextView)toast.getView(); + view.setText(message); + view.invalidate(); // make sure the toast is redrawn + toast.setView(view); + } + else { + if( clear_toast != null && clear_toast.toast != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "cancel last toast: " + clear_toast.toast); + clear_toast.toast.cancel(); + } + toast = new Toast(activity); + if( MyDebug.LOG ) + Log.d(TAG, "created new toast: " + toast); + if( clear_toast != null ) + clear_toast.toast = toast; + View text = new RotatedTextView(message, activity); + toast.setView(text); + } + toast.setDuration(Toast.LENGTH_SHORT); + toast.show(); + last_toast = toast; + } + }); + } + + public void setUIRotation(int ui_rotation) { + if( MyDebug.LOG ) + Log.d(TAG, "setUIRotation"); + this.ui_rotation = ui_rotation; + } + + public int getUIRotation() { + return this.ui_rotation; + } + + /** If geotagging is enabled, pass the location info to the camera controller (for photos). + */ + private void updateParametersFromLocation() { + if( MyDebug.LOG ) + Log.d(TAG, "updateParametersFromLocation"); + if( camera_controller != null ) { + boolean store_location = applicationInterface.getGeotaggingPref(); + if( store_location && applicationInterface.getLocation() != null ) { + Location location = applicationInterface.getLocation(); + if( MyDebug.LOG ) { + Log.d(TAG, "updating parameters from location..."); + Log.d(TAG, "lat " + location.getLatitude() + " long " + location.getLongitude() + " accuracy " + location.getAccuracy() + " timestamp " + location.getTime()); + } + camera_controller.setLocationInfo(location); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "removing location data from parameters..."); + camera_controller.removeLocationInfo(); + } + } + } + + public boolean isVideo() { + return is_video; + } + + public boolean isVideoRecording() { + return video_recorder != null && video_start_time_set; + } + + public boolean isVideoRecordingPaused() { + return isVideoRecording() && video_recorder_is_paused; + } + + public long getVideoTime() { + if( this.isVideoRecordingPaused() ) { + return video_accumulated_time; + } + long time_now = System.currentTimeMillis(); + return time_now - video_start_time + video_accumulated_time; + } + + public long getVideoAccumulatedTime() { + return video_accumulated_time; + } + + public boolean isTakingPhoto() { + return this.phase == PHASE_TAKING_PHOTO; + } + + public boolean usingCamera2API() { + return this.using_android_l; + } + + public CameraController getCameraController() { + return this.camera_controller; + } + + public CameraControllerManager getCameraControllerManager() { + return this.camera_controller_manager; + } + + public boolean supportsFocus() { + return this.supported_focus_values != null; + } + + public boolean supportsFlash() { + return this.supported_flash_values != null; + } + + public boolean supportsExposureLock() { + return this.is_exposure_lock_supported; + } + + public boolean isExposureLocked() { + return this.is_exposure_locked; + } + + public boolean supportsZoom() { + return this.has_zoom; + } + + public int getMaxZoom() { + return this.max_zoom_factor; + } + + public boolean hasFocusArea() { + return this.has_focus_area; + } + + public Pair getFocusPos() { + return new Pair<>(focus_screen_x, focus_screen_y); + } + + public int getMaxNumFocusAreas() { + return this.max_num_focus_areas; + } + + public boolean isTakingPhotoOrOnTimer() { + //return this.is_taking_photo; + return this.phase == PHASE_TAKING_PHOTO || this.phase == PHASE_TIMER; + } + + public boolean isOnTimer() { + //return this.is_taking_photo_on_timer; + return this.phase == PHASE_TIMER; + } + + public long getTimerEndTime() { + return take_photo_time; + } + + public boolean isPreviewPaused() { + return this.phase == PHASE_PREVIEW_PAUSED; + } + + public boolean isPreviewStarted() { + return this.is_preview_started; + } + + public boolean isFocusWaiting() { + return focus_success == FOCUS_WAITING; + } + + public boolean isFocusRecentSuccess() { + return focus_success == FOCUS_SUCCESS; + } + + public long timeSinceStartedAutoFocus() { + if( focus_started_time != -1 ) + return System.currentTimeMillis() - focus_started_time; + return 0; + } + + public boolean isFocusRecentFailure() { + return focus_success == FOCUS_FAILED; + } + + /** Whether we can skip the autofocus before taking a photo. + */ + private boolean recentlyFocused() { + return this.successfully_focused && System.currentTimeMillis() < this.successfully_focused_time + 5000; + } + + public CameraController.Face [] getFacesDetected() { + // FindBugs warns about returning the array directly, but in fact we need to return direct access rather than copying, so that the on-screen display of faces rectangles updates + return this.faces_detected; + } + + /** Returns the current zoom factor of the camera. Always returns 1.0f if zoom isn't supported. + */ + public float getZoomRatio() { + if( zoom_ratios == null ) + return 1.0f; + int zoom_factor = camera_controller.getZoom(); + return this.zoom_ratios.get(zoom_factor)/100.0f; + } +} diff --git a/src/main/java/net/sourceforge/opencamera/Preview/VideoQualityHandler.java b/src/main/java/net/sourceforge/opencamera/Preview/VideoQualityHandler.java new file mode 100644 index 00000000..6039fde5 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/Preview/VideoQualityHandler.java @@ -0,0 +1,159 @@ +package net.sourceforge.opencamera.Preview; + +import android.media.CamcorderProfile; +import android.util.Log; + +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.MyDebug; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Handles video quality options. + * Note that this class should avoid calls to the Android API, so we can perform local unit testing + * on it. + */ +public class VideoQualityHandler { + private static final String TAG = "VideoQualityHandler"; + + public static class Dimension2D { + final int width; + final int height; + + public Dimension2D(int width, int height) { + this.width = width; + this.height = height; + } + } + + // video_quality can either be: + // - an int, in which case it refers to a CamcorderProfile + // - of the form [CamcorderProfile]_r[width]x[height] - we use the CamcorderProfile as a base, and override the video resolution - this is needed to support resolutions which don't have corresponding camcorder profiles + private List video_quality; + private int current_video_quality = -1; // this is an index into the video_quality array, or -1 if not found (though this shouldn't happen?) + private List video_sizes; + + void resetCurrentQuality() { + video_quality = null; + current_video_quality = -1; + } + + /** Initialises the class with the available video profiles and resolutions. The user should first + * set the video sizes via setVideoSizes(). + * @param profiles A list of qualities (see CamcorderProfile.QUALITY_*). Should be supplied in + * order from highest to lowest quality. + * @param dimensions A corresponding list of the width/height for that quality (as given by + * videoFrameWidth, videoFrameHeight in the profile returned by CamcorderProfile.get()). + */ + public void initialiseVideoQualityFromProfiles(List profiles, List dimensions) { + if( MyDebug.LOG ) + Log.d(TAG, "initialiseVideoQualityFromProfiles()"); + video_quality = new ArrayList<>(); + boolean done_video_size[] = null; + if( video_sizes != null ) { + done_video_size = new boolean[video_sizes.size()]; + for(int i=0;i, Serializable { + private static final long serialVersionUID = 5802214721033718212L; + + @Override + public int compare(final CameraController.Size a, final CameraController.Size b) { + return b.width * b.height - a.width * a.height; + } + } + + public void sortVideoSizes() { + if( MyDebug.LOG ) + Log.d(TAG, "sortVideoSizes()"); + Collections.sort(this.video_sizes, new SortVideoSizesComparator()); + if( MyDebug.LOG ) { + for(CameraController.Size size : video_sizes) { + Log.d(TAG, " supported video size: " + size.width + ", " + size.height); + } + } + } + + private void addVideoResolutions(boolean done_video_size[], int base_profile, int min_resolution_w, int min_resolution_h) { + if( video_sizes == null ) { + return; + } + if( MyDebug.LOG ) + Log.d(TAG, "profile " + base_profile + " is resolution " + min_resolution_w + " x " + min_resolution_h); + for(int i=0;i= min_resolution_w*min_resolution_h ) { + String str = "" + base_profile + "_r" + size.width + "x" + size.height; + video_quality.add(str); + done_video_size[i] = true; + if( MyDebug.LOG ) + Log.d(TAG, "added: " + str); + } + } + } + + public List getSupportedVideoQuality() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedVideoQuality"); + return this.video_quality; + } + + public int getCurrentVideoQualityIndex() { + if( MyDebug.LOG ) + Log.d(TAG, "getCurrentVideoQualityIndex"); + return this.current_video_quality; + } + + public void setCurrentVideoQualityIndex(int current_video_quality) { + if( MyDebug.LOG ) + Log.d(TAG, "setCurrentVideoQualityIndex: " + current_video_quality); + this.current_video_quality = current_video_quality; + } + + public String getCurrentVideoQuality() { + if( current_video_quality == -1 ) + return null; + return video_quality.get(current_video_quality); + } + + public List getSupportedVideoSizes() { + if( MyDebug.LOG ) + Log.d(TAG, "getSupportedVideoSizes"); + return this.video_sizes; + } + + public void setVideoSizes(List video_sizes) { + this.video_sizes = video_sizes; + this.sortVideoSizes(); + } + +} diff --git a/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java b/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java new file mode 100644 index 00000000..cd809f39 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java @@ -0,0 +1,136 @@ +package net.sourceforge.opencamera; + +import java.util.ArrayList; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +/** Handles a history of save locations. + */ +public class SaveLocationHistory { + private static final String TAG = "SaveLocationHistory"; + private final MainActivity main_activity; + private final String pref_base; + private final ArrayList save_location_history = new ArrayList<>(); + + /** Creates a new SaveLocationHistory class. This manages a history of save folder locations. + * @param main_activity MainActivity. + * @param pref_base String to use for shared preferences. + * @param folder_name The current save folder. + */ + SaveLocationHistory(MainActivity main_activity, String pref_base, String folder_name) { + if( MyDebug.LOG ) + Log.d(TAG, "pref_base: " + pref_base); + this.main_activity = main_activity; + this.pref_base = pref_base; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + + // read save locations + save_location_history.clear(); + int save_location_history_size = sharedPreferences.getInt(pref_base + "_size", 0); + if( MyDebug.LOG ) + Log.d(TAG, "save_location_history_size: " + save_location_history_size); + for(int i=0;i 6 ) { + save_location_history.remove(0); + } + writeSaveLocations(); + if( MyDebug.LOG ) { + Log.d(TAG, "updateFolderHistory exit:"); + Log.d(TAG, "save_location_history size: " + save_location_history.size()); + for(int i=0;i= Build.VERSION_CODES.N ) { + if( MyDebug.LOG ) + Log.d(TAG, "broadcasts deprecated on Android 7 onwards, so don't send them"); + // see note above; the intents won't be delivered, so might as well save the trouble of trying to send them + } + else if( is_new_picture ) { + // note, we reference the string directly rather than via Camera.ACTION_NEW_PICTURE, as the latter class is now deprecated - but we still need to broadcast the string for other apps + context.sendBroadcast(new Intent( "android.hardware.action.NEW_PICTURE" , uri)); + // for compatibility with some apps - apparently this is what used to be broadcast on Android? + context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri)); + + if( MyDebug.LOG ) // this code only used for debugging/logging + { + String[] CONTENT_PROJECTION = { Images.Media.DATA, Images.Media.DISPLAY_NAME, Images.Media.MIME_TYPE, Images.Media.SIZE, Images.Media.DATE_TAKEN, Images.Media.DATE_ADDED }; + Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION, null, null, null); + if( c == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [1]: " + uri); + } + else if( !c.moveToFirst() ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [2]: " + uri); + } + else { + String file_path = c.getString(c.getColumnIndex(Images.Media.DATA)); + String file_name = c.getString(c.getColumnIndex(Images.Media.DISPLAY_NAME)); + String mime_type = c.getString(c.getColumnIndex(Images.Media.MIME_TYPE)); + long date_taken = c.getLong(c.getColumnIndex(Images.Media.DATE_TAKEN)); + long date_added = c.getLong(c.getColumnIndex(Images.Media.DATE_ADDED)); + Log.d(TAG, "file_path: " + file_path); + Log.d(TAG, "file_name: " + file_name); + Log.d(TAG, "mime_type: " + mime_type); + Log.d(TAG, "date_taken: " + date_taken); + Log.d(TAG, "date_added: " + date_added); + c.close(); + } + } + /*{ + // hack: problem on Camera2 API (at least on Nexus 6) that if geotagging is enabled, then the resultant image has incorrect Exif TAG_GPS_DATESTAMP (GPSDateStamp) set (tends to be around 2038 - possibly a driver bug of casting long to int?) + // whilst we don't yet correct for that bug, the more immediate problem is that it also messes up the DATE_TAKEN field in the media store, which messes up Gallery apps + // so for now, we correct it based on the DATE_ADDED value. + String[] CONTENT_PROJECTION = { Images.Media.DATE_ADDED }; + Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION, null, null, null); + if( c == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [1]: " + uri); + } + else if( !c.moveToFirst() ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [2]: " + uri); + } + else { + long date_added = c.getLong(c.getColumnIndex(Images.Media.DATE_ADDED)); + if( MyDebug.LOG ) + Log.e(TAG, "replace date_taken with date_added: " + date_added); + ContentValues values = new ContentValues(); + values.put(Images.Media.DATE_TAKEN, date_added*1000); + context.getContentResolver().update(uri, values, null, null); + c.close(); + } + }*/ + } + else if( is_new_video ) { + context.sendBroadcast(new Intent("android.hardware.action.NEW_VIDEO", uri)); + + /*String[] CONTENT_PROJECTION = { Video.Media.DURATION }; + Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION, null, null, null); + if( c == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [1]: " + uri); + } + else if( !c.moveToFirst() ) { + if( MyDebug.LOG ) + Log.e(TAG, "Couldn't resolve given uri [2]: " + uri); + } + else { + long duration = c.getLong(c.getColumnIndex(Video.Media.DURATION)); + if( MyDebug.LOG ) + Log.e(TAG, "replace duration: " + duration); + ContentValues values = new ContentValues(); + values.put(Video.Media.DURATION, 1000); + context.getContentResolver().update(uri, values, null, null); + c.close(); + }*/ + } + } + + /*public Uri broadcastFileRaw(File file, Date current_date, Location location) { + if( MyDebug.LOG ) + Log.d(TAG, "broadcastFileRaw: " + file.getAbsolutePath()); + ContentValues values = new ContentValues(); + values.put(ImageColumns.TITLE, file.getName().substring(0, file.getName().lastIndexOf("."))); + values.put(ImageColumns.DISPLAY_NAME, file.getName()); + values.put(ImageColumns.DATE_TAKEN, current_date.getTime()); + values.put(ImageColumns.MIME_TYPE, "image/dng"); + //values.put(ImageColumns.MIME_TYPE, "image/jpeg"); + if( location != null ) { + values.put(ImageColumns.LATITUDE, location.getLatitude()); + values.put(ImageColumns.LONGITUDE, location.getLongitude()); + } + // leave ORIENTATION for now - this doesn't seem to get inserted for JPEGs anyway (via MediaScannerConnection.scanFile()) + values.put(ImageColumns.DATA, file.getAbsolutePath()); + //values.put(ImageColumns.DATA, "/storage/emulated/0/DCIM/OpenCamera/blah.dng"); + Uri uri = null; + try { + uri = context.getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values); + if( MyDebug.LOG ) + Log.d(TAG, "inserted media uri: " + uri); + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); + } + catch (Throwable th) { + // This can happen when the external volume is already mounted, but + // MediaScanner has not notify MediaProvider to add that volume. + // The picture is still safe and MediaScanner will find it and + // insert it into MediaProvider. The only problem is that the user + // cannot click the thumbnail to review the picture. + Log.e(TAG, "Failed to write MediaStore" + th); + } + return uri; + }*/ + + /** Sends a "broadcast" for the new file. This is necessary so that Android recognises the new file without needing a reboot: + * - So that they show up when connected to a PC using MTP. + * - For JPEGs, so that they show up in gallery applications. + * - This also calls announceUri() on the resultant Uri for the new file. + * - Note this should also be called after deleting a file. + * - Note that for DNG files, MediaScannerConnection.scanFile() doesn't result in the files being shown in gallery applications. + * This may well be intentional, since most gallery applications won't read DNG files anyway. But it's still important to + * call this function for DNGs, so that they show up on MTP. + */ + public void broadcastFile(final File file, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned) { + if( MyDebug.LOG ) + Log.d(TAG, "broadcastFile: " + file.getAbsolutePath()); + // note that the new method means that the new folder shows up as a file when connected to a PC via MTP (at least tested on Windows 8) + if( file.isDirectory() ) { + //this.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(file))); + // ACTION_MEDIA_MOUNTED no longer allowed on Android 4.4! Gives: SecurityException: Permission Denial: not allowed to send broadcast android.intent.action.MEDIA_MOUNTED + // note that we don't actually need to broadcast anything, the folder and contents appear straight away (both in Gallery on device, and on a PC when connecting via MTP) + // also note that we definitely don't want to broadcast ACTION_MEDIA_SCANNER_SCAN_FILE or use scanFile() for folders, as this means the folder shows up as a file on a PC via MTP (and isn't fixed by rebooting!) + } + else { + // both of these work fine, but using MediaScannerConnection.scanFile() seems to be preferred over sending an intent + //context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + failed_to_scan = true; // set to true until scanned okay + if( MyDebug.LOG ) + Log.d(TAG, "failed_to_scan set to true"); + MediaScannerConnection.scanFile(context, new String[] { file.getAbsolutePath() }, null, + new MediaScannerConnection.OnScanCompletedListener() { + public void onScanCompleted(String path, Uri uri) { + failed_to_scan = false; + if( MyDebug.LOG ) { + Log.d(TAG, "Scanned " + path + ":"); + Log.d(TAG, "-> uri=" + uri); + } + if( set_last_scanned ) { + last_media_scanned = uri; + if( MyDebug.LOG ) + Log.d(TAG, "set last_media_scanned to " + last_media_scanned); + } + announceUri(uri, is_new_picture, is_new_video); + + // it seems caller apps seem to prefer the content:// Uri rather than one based on a File + // update for Android 7: seems that passing file uris is now restricted anyway, see https://code.google.com/p/android/issues/detail?id=203555 + Activity activity = (Activity)context; + String action = activity.getIntent().getAction(); + if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "from video capture intent"); + Intent output = new Intent(); + output.setData(uri); + activity.setResult(Activity.RESULT_OK, output); + activity.finish(); + } + } + } + ); + } + } + + boolean isUsingSAF() { + // check Android version just to be safe + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if( sharedPreferences.getBoolean(PreferenceKeys.getUsingSAFPreferenceKey(), false) ) { + return true; + } + } + return false; + } + + // only valid if !isUsingSAF() + String getSaveLocation() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + } + + // only valid if isUsingSAF() + String getSaveLocationSAF() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getString(PreferenceKeys.getSaveLocationSAFPreferenceKey(), ""); + } + + // only valid if isUsingSAF() + private Uri getTreeUriSAF() { + String folder_name = getSaveLocationSAF(); + return Uri.parse(folder_name); + } + + // valid if whether or not isUsingSAF() + // but note that if isUsingSAF(), this may return null - it can't be assumed that there is a File corresponding to the SAF Uri + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + File getImageFolder() { + File file; + if( isUsingSAF() ) { + Uri uri = getTreeUriSAF(); + /*if( MyDebug.LOG ) + Log.d(TAG, "uri: " + uri);*/ + file = getFileFromDocumentUriSAF(uri, true); + } + else { + String folder_name = getSaveLocation(); + file = getImageFolder(folder_name); + } + return file; + } + + public static File getBaseFolder() { + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + } + + // only valid if !isUsingSAF() + public static File getImageFolder(String folder_name) { + File file; + if( folder_name.length() > 0 && folder_name.lastIndexOf('/') == folder_name.length()-1 ) { + // ignore final '/' character + folder_name = folder_name.substring(0, folder_name.length()-1); + } + //if( folder_name.contains("/") ) { + if( folder_name.startsWith("/") ) { + file = new File(folder_name); + } + else { + file = new File(getBaseFolder(), folder_name); + } + return file; + } + + // only valid if isUsingSAF() + // This function should only be used as a last resort - we shouldn't generally assume that a Uri represents an actual File, and instead. + // However this is needed for a workaround to the fact that deleting a document file doesn't remove it from MediaStore. + // See: + // http://stackoverflow.com/questions/21605493/storage-access-framework-does-not-update-mediascanner-mtp + // http://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework/ + // only valid if isUsingSAF() + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + File getFileFromDocumentUriSAF(Uri uri, boolean is_folder) { + if( MyDebug.LOG ) { + Log.d(TAG, "getFileFromDocumentUriSAF: " + uri); + Log.d(TAG, "is_folder?: " + is_folder); + } + File file = null; + if( "com.android.externalstorage.documents".equals(uri.getAuthority()) ) { + final String id = is_folder ? DocumentsContract.getTreeDocumentId(uri) : DocumentsContract.getDocumentId(uri); + if( MyDebug.LOG ) + Log.d(TAG, "id: " + id); + String [] split = id.split(":"); + if( split.length >= 2 ) { + String type = split[0]; + String path = split[1]; + /*if( MyDebug.LOG ) { + Log.d(TAG, "type: " + type); + Log.d(TAG, "path: " + path); + }*/ + File [] storagePoints = new File("/storage").listFiles(); + + if( "primary".equalsIgnoreCase(type) ) { + final File externalStorage = Environment.getExternalStorageDirectory(); + file = new File(externalStorage, path); + } + for(int i=0;storagePoints != null && i 0 ) { + index = "_" + count; // try to find a unique filename + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean useZuluTime = sharedPreferences.getString(PreferenceKeys.getSaveZuluTimePreferenceKey(), "local").equals("zulu"); + String timeStamp; + if( useZuluTime ) { + SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd_HHmmss'Z'", Locale.US); + fmt.setTimeZone(TimeZone.getTimeZone("UTC")); + timeStamp = fmt.format(current_date); + } + else { + timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(current_date); + } + String mediaFilename; + if( type == MEDIA_TYPE_IMAGE ) { + String prefix = sharedPreferences.getString(PreferenceKeys.getSavePhotoPrefixPreferenceKey(), "IMG_"); + mediaFilename = prefix + timeStamp + suffix + index + "." + extension; + } + else if( type == MEDIA_TYPE_VIDEO ) { + String prefix = sharedPreferences.getString(PreferenceKeys.getSaveVideoPrefixPreferenceKey(), "VID_"); + mediaFilename = prefix + timeStamp + suffix + index + "." + extension; + } + else { + // throw exception as this is a programming error + if( MyDebug.LOG ) + Log.e(TAG, "unknown type: " + type); + throw new RuntimeException(); + } + return mediaFilename; + } + + // only valid if !isUsingSAF() + @SuppressLint("SimpleDateFormat") + File createOutputMediaFile(int type, String suffix, String extension, Date current_date) throws IOException { + File mediaStorageDir = getImageFolder(); + + // Create the storage directory if it does not exist + if( !mediaStorageDir.exists() ) { + if( !mediaStorageDir.mkdirs() ) { + if( MyDebug.LOG ) + Log.e(TAG, "failed to create directory"); + throw new IOException(); + } + broadcastFile(mediaStorageDir, false, false, false); + } + + // Create a media file name + File mediaFile = null; + for(int count=0;count<100;count++) { + String mediaFilename = createMediaFilename(type, suffix, count, extension, current_date); + mediaFile = new File(mediaStorageDir.getPath() + File.separator + mediaFilename); + if( !mediaFile.exists() ) { + break; + } + } + + if( MyDebug.LOG ) { + Log.d(TAG, "getOutputMediaFile returns: " + mediaFile); + } + if( mediaFile == null ) + throw new IOException(); + return mediaFile; + } + + // only valid if isUsingSAF() + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + Uri createOutputFileSAF(String filename, String mimeType) throws IOException { + try { + Uri treeUri = getTreeUriSAF(); + if( MyDebug.LOG ) + Log.d(TAG, "treeUri: " + treeUri); + Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); + if( MyDebug.LOG ) + Log.d(TAG, "docUri: " + docUri); + // note that DocumentsContract.createDocument will automatically append to the filename if it already exists + Uri fileUri = DocumentsContract.createDocument(context.getContentResolver(), docUri, mimeType, filename); + if( MyDebug.LOG ) + Log.d(TAG, "returned fileUri: " + fileUri); + if( fileUri == null ) + throw new IOException(); + return fileUri; + } + catch(IllegalArgumentException e) { + // DocumentsContract.getTreeDocumentId throws this if URI is invalid + if( MyDebug.LOG ) + Log.e(TAG, "createOutputMediaFileSAF failed"); + e.printStackTrace(); + throw new IOException(); + } + } + + // only valid if isUsingSAF() + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + Uri createOutputMediaFileSAF(int type, String suffix, String extension, Date current_date) throws IOException { + String mimeType; + if( type == MEDIA_TYPE_IMAGE ) { + if( extension.equals("dng") ) { + mimeType = "image/dng"; + //mimeType = "image/x-adobe-dng"; + } + else + mimeType = "image/jpeg"; + } + else if( type == MEDIA_TYPE_VIDEO ) { + mimeType = "video/mp4"; + } + else { + // throw exception as this is a programming error + if( MyDebug.LOG ) + Log.e(TAG, "unknown type: " + type); + throw new RuntimeException(); + } + // note that DocumentsContract.createDocument will automatically append to the filename if it already exists + String mediaFilename = createMediaFilename(type, suffix, 0, extension, current_date); + return createOutputFileSAF(mediaFilename, mimeType); + } + + static class Media { + final long id; + final boolean video; + final Uri uri; + final long date; + final int orientation; + + Media(long id, boolean video, Uri uri, long date, int orientation) { + this.id = id; + this.video = video; + this.uri = uri; + this.date = date; + this.orientation = orientation; + } + } + + private Media getLatestMedia(boolean video) { + if( MyDebug.LOG ) + Log.d(TAG, "getLatestMedia: " + (video ? "video" : "images")); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) { + // needed for Android 6, in case users deny storage permission, otherwise we get java.lang.SecurityException from ContentResolver.query() + // see https://developer.android.com/training/permissions/requesting.html + // we now request storage permission before opening the camera, but keep this here just in case + // we restrict check to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener() + if( MyDebug.LOG ) + Log.e(TAG, "don't have READ_EXTERNAL_STORAGE permission"); + return null; + } + Media media = null; + Uri baseUri = video ? Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + //Uri query = baseUri.buildUpon().appendQueryParameter("limit", "1").build(); + Uri query = baseUri; + final int column_id_c = 0; + final int column_date_taken_c = 1; + final int column_data_c = 2; + final int column_orientation_c = 3; + String [] projection = video ? new String[] {VideoColumns._ID, VideoColumns.DATE_TAKEN, VideoColumns.DATA} : new String[] {ImageColumns._ID, ImageColumns.DATE_TAKEN, ImageColumns.DATA, ImageColumns.ORIENTATION}; + String selection = video ? "" : ImageColumns.MIME_TYPE + "='image/jpeg'"; + String order = video ? VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC" : ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC"; + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(query, projection, selection, null, order); + if( cursor != null && cursor.moveToFirst() ) { + if( MyDebug.LOG ) + Log.d(TAG, "found: " + cursor.getCount()); + // now sorted in order of date - scan to most recent one in the Open Camera save folder + boolean found = false; + File save_folder = getImageFolder(); // may be null if using SAF + String save_folder_string = save_folder == null ? null : save_folder.getAbsolutePath() + File.separator; + if( MyDebug.LOG ) + Log.d(TAG, "save_folder_string: " + save_folder_string); + do { + String path = cursor.getString(column_data_c); + if( MyDebug.LOG ) + Log.d(TAG, "path: " + path); + // path may be null on Android 4.4!: http://stackoverflow.com/questions/3401579/get-filename-and-path-from-uri-from-mediastore + if( save_folder_string == null || (path != null && path.contains(save_folder_string) ) ) { + if( MyDebug.LOG ) + Log.d(TAG, "found most recent in Open Camera folder"); + // we filter files with dates in future, in case there exists an image in the folder with incorrect datestamp set to the future + // we allow up to 2 days in future, to avoid risk of issues to do with timezone etc + long date = cursor.getLong(column_date_taken_c); + long current_time = System.currentTimeMillis(); + if( date > current_time + 172800000 ) { + if( MyDebug.LOG ) + Log.d(TAG, "skip date in the future!"); + } + else { + found = true; + break; + } + } + } while( cursor.moveToNext() ); + if( !found ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't find suitable in Open Camera folder, so just go with most recent"); + cursor.moveToFirst(); + } + long id = cursor.getLong(column_id_c); + long date = cursor.getLong(column_date_taken_c); + int orientation = video ? 0 : cursor.getInt(column_orientation_c); + Uri uri = ContentUris.withAppendedId(baseUri, id); + if( MyDebug.LOG ) + Log.d(TAG, "found most recent uri for " + (video ? "video" : "images") + ": " + uri); + media = new Media(id, video, uri, date, orientation); + } + } + catch(SQLiteException e) { + // had this reported on Google Play from getContentResolver().query() call + if( MyDebug.LOG ) + Log.e(TAG, "SQLiteException trying to find latest media"); + e.printStackTrace(); + } + finally { + if( cursor != null ) { + cursor.close(); + } + } + return media; + } + + Media getLatestMedia() { + Media image_media = getLatestMedia(false); + Media video_media = getLatestMedia(true); + Media media = null; + if( image_media != null && video_media == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "only found images"); + media = image_media; + } + else if( image_media == null && video_media != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "only found videos"); + media = video_media; + } + else if( image_media != null && video_media != null ) { + if( MyDebug.LOG ) { + Log.d(TAG, "found images and videos"); + Log.d(TAG, "latest image date: " + image_media.date); + Log.d(TAG, "latest video date: " + video_media.date); + } + if( image_media.date >= video_media.date ) { + if( MyDebug.LOG ) + Log.d(TAG, "latest image is newer"); + media = image_media; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "latest video is newer"); + media = video_media; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "return latest media: " + media); + return media; + } +} diff --git a/src/main/java/net/sourceforge/opencamera/TakePhoto.java b/src/main/java/net/sourceforge/opencamera/TakePhoto.java new file mode 100644 index 00000000..b57c276b --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/TakePhoto.java @@ -0,0 +1,36 @@ +package net.sourceforge.opencamera; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/** Entry Activity for the "take photo" widget (see MyWidgetProviderTakePhoto). + * This redirects to MainActivity, but uses an intent extra/bundle to pass the + * "take photo" request. + */ +public class TakePhoto extends Activity { + private static final String TAG = "TakePhoto"; + public static final String TAKE_PHOTO = "net.sourceforge.opencamera.TAKE_PHOTO"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + if( MyDebug.LOG ) + Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(TAKE_PHOTO, true); + this.startActivity(intent); + if( MyDebug.LOG ) + Log.d(TAG, "finish"); + this.finish(); + } + + protected void onResume() { + if( MyDebug.LOG ) + Log.d(TAG, "onResume"); + super.onResume(); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/TextFormatter.java b/src/main/java/net/sourceforge/opencamera/TextFormatter.java new file mode 100644 index 00000000..84b94563 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/TextFormatter.java @@ -0,0 +1,102 @@ +package net.sourceforge.opencamera; + +import android.content.Context; +import android.location.Location; +import android.util.Log; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** Handles various text formatting options, used for photo stamp and video subtitles. + */ +public class TextFormatter { + private static final String TAG = "TextFormatter"; + + private final Context context; + private final DecimalFormat decimalFormat = new DecimalFormat("#0.0"); + + TextFormatter(Context context) { + this.context = context; + } + + /** Formats the date according to the user preference preference_stamp_dateformat. + * Returns "" if preference_stamp_dateformat is "preference_stamp_dateformat_none". + */ + public static String getDateString(String preference_stamp_dateformat, Date date) { + String date_stamp = ""; + if( !preference_stamp_dateformat.equals("preference_stamp_dateformat_none") ) { + if( preference_stamp_dateformat.equals("preference_stamp_dateformat_yyyymmdd") ) + date_stamp = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(date); + else if( preference_stamp_dateformat.equals("preference_stamp_dateformat_ddmmyyyy") ) + date_stamp = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(date); + else if( preference_stamp_dateformat.equals("preference_stamp_dateformat_mmddyyyy") ) + date_stamp = new SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()).format(date); + else // default + date_stamp = DateFormat.getDateInstance().format(date); + } + return date_stamp; + } + + /** Formats the time according to the user preference preference_stamp_timeformat. + * Returns "" if preference_stamp_timeformat is "preference_stamp_timeformat_none". + */ + public static String getTimeString(String preference_stamp_timeformat, Date date) { + String time_stamp = ""; + if( !preference_stamp_timeformat.equals("preference_stamp_timeformat_none") ) { + if( preference_stamp_timeformat.equals("preference_stamp_timeformat_12hour") ) + time_stamp = new SimpleDateFormat("hh:mm:ss a", Locale.getDefault()).format(date); + else if( preference_stamp_timeformat.equals("preference_stamp_timeformat_24hour") ) + time_stamp = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(date); + else // default + time_stamp = DateFormat.getTimeInstance().format(date); + } + return time_stamp; + } + + /** Formats the GPS information according to the user preference_stamp_gpsformat preference_stamp_timeformat. + * Returns "" if preference_stamp_gpsformat is "preference_stamp_gpsformat_none", or both store_location and + * store_geo_direction are false. + */ + public String getGPSString(String preference_stamp_gpsformat, boolean store_location, Location location, boolean store_geo_direction, double geo_direction) { + String gps_stamp = ""; + if( !preference_stamp_gpsformat.equals("preference_stamp_gpsformat_none") ) { + if( store_location ) { + if( MyDebug.LOG ) + Log.d(TAG, "location: " + location); + if( preference_stamp_gpsformat.equals("preference_stamp_gpsformat_dms") ) + gps_stamp += LocationSupplier.locationToDMS(location.getLatitude()) + ", " + LocationSupplier.locationToDMS(location.getLongitude()); + else + gps_stamp += Location.convert(location.getLatitude(), Location.FORMAT_DEGREES) + ", " + Location.convert(location.getLongitude(), Location.FORMAT_DEGREES); + if( location.hasAltitude() ) { + gps_stamp += ", " + decimalFormat.format(location.getAltitude()) + context.getResources().getString(R.string.metres_abbreviation); + } + } + if( store_geo_direction ) { + float geo_angle = (float)Math.toDegrees(geo_direction); + if( geo_angle < 0.0f ) { + geo_angle += 360.0f; + } + if( MyDebug.LOG ) + Log.d(TAG, "geo_angle: " + geo_angle); + if( gps_stamp.length() > 0 ) + gps_stamp += ", "; + gps_stamp += "" + Math.round(geo_angle) + (char)0x00B0; + } + } + if( MyDebug.LOG ) + Log.d(TAG, "gps_stamp: " + gps_stamp); + return gps_stamp; + } + + public static String formatTimeMS(long time_ms) { + int ms = (int) (time_ms) % 1000 ; + int seconds = (int) (time_ms / 1000) % 60 ; + int minutes = (int) ((time_ms / (1000*60)) % 60); + int hours = (int) ((time_ms / (1000*60*60))); + return String.format(Locale.getDefault(), "%02d:%02d:%02d,%03d", hours, minutes, seconds, ms); + } + +} diff --git a/src/main/java/net/sourceforge/opencamera/ToastBoxer.java b/src/main/java/net/sourceforge/opencamera/ToastBoxer.java new file mode 100644 index 00000000..61cde86d --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/ToastBoxer.java @@ -0,0 +1,12 @@ +package net.sourceforge.opencamera; + +import android.widget.Toast; + +/** Allows methods to update a Toast with a new Toast. + */ +public class ToastBoxer { + public Toast toast; + + public ToastBoxer() { + } +} diff --git a/src/main/java/net/sourceforge/opencamera/UI/DrawPreview.java b/src/main/java/net/sourceforge/opencamera/UI/DrawPreview.java new file mode 100644 index 00000000..15cee692 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/UI/DrawPreview.java @@ -0,0 +1,1244 @@ +package net.sourceforge.opencamera.UI; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.util.Calendar; +import java.util.Locale; + +import net.sourceforge.opencamera.MainActivity; +import net.sourceforge.opencamera.MyApplicationInterface; +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.PreferenceKeys; +import net.sourceforge.opencamera.R; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.Preview.Preview; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.BatteryManager; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.Pair; +import android.view.Surface; +import android.view.View; +import android.widget.ImageButton; + +public class DrawPreview { + private static final String TAG = "DrawPreview"; + + private final MainActivity main_activity; + private final MyApplicationInterface applicationInterface; + + private final Paint p = new Paint(); + private final RectF face_rect = new RectF(); + private final RectF draw_rect = new RectF(); + private final int [] gui_location = new int[2]; + private final DecimalFormat decimalFormat = new DecimalFormat("#0.0"); + private final float stroke_width; + + private float free_memory_gb = -1.0f; + private long last_free_memory_time; + + private final IntentFilter battery_ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + private boolean has_battery_frac; + private float battery_frac; + private long last_battery_time; + + private Bitmap location_bitmap; + private Bitmap location_off_bitmap; + private final Rect location_dest = new Rect(); + + private Bitmap last_thumbnail; // thumbnail of last picture taken + private volatile boolean thumbnail_anim; // whether we are displaying the thumbnail animation; must be volatile for test project reading the state + private long thumbnail_anim_start_ms = -1; // time that the thumbnail animation started + private final RectF thumbnail_anim_src_rect = new RectF(); + private final RectF thumbnail_anim_dst_rect = new RectF(); + private final Matrix thumbnail_anim_matrix = new Matrix(); + + private boolean show_last_image; + private final RectF last_image_src_rect = new RectF(); + private final RectF last_image_dst_rect = new RectF(); + private final Matrix last_image_matrix = new Matrix(); + + private long ae_started_scanning_ms = -1; // time when ae started scanning + + private boolean taking_picture; // true iff camera is in process of capturing a picture (including any necessary prior steps such as autofocus, flash/precapture) + private boolean capture_started; // true iff the camera is capturing + private long capture_started_time_ms; // time whe capture_started was set to true + private boolean front_screen_flash; // true iff the front screen display should maximise to simulate flash + + private boolean continuous_focus_moving; + private long continuous_focus_moving_ms; + + public DrawPreview(MainActivity main_activity, MyApplicationInterface applicationInterface) { + if( MyDebug.LOG ) + Log.d(TAG, "DrawPreview"); + this.main_activity = main_activity; + this.applicationInterface = applicationInterface; + + p.setAntiAlias(true); + p.setStrokeCap(Paint.Cap.ROUND); + final float scale = getContext().getResources().getDisplayMetrics().density; + this.stroke_width = (1.0f * scale + 0.5f); // convert dps to pixels + p.setStrokeWidth(stroke_width); + + location_bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.earth); + location_off_bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.earth_off); + } + + public void onDestroy() { + if( MyDebug.LOG ) + Log.d(TAG, "onDestroy"); + // clean up just in case + if( location_bitmap != null ) { + location_bitmap.recycle(); + location_bitmap = null; + } + if( location_off_bitmap != null ) { + location_off_bitmap.recycle(); + location_off_bitmap = null; + } + } + + private Context getContext() { + return main_activity; + } + + public void updateThumbnail(Bitmap thumbnail) { + if( MyDebug.LOG ) + Log.d(TAG, "updateThumbnail"); + if( applicationInterface.getThumbnailAnimationPref() ) { + if( MyDebug.LOG ) + Log.d(TAG, "thumbnail_anim started"); + thumbnail_anim = true; + thumbnail_anim_start_ms = System.currentTimeMillis(); + } + Bitmap old_thumbnail = this.last_thumbnail; + this.last_thumbnail = thumbnail; + if( old_thumbnail != null ) { + // only recycle after we've set the new thumbnail + old_thumbnail.recycle(); + } + } + + public boolean hasThumbnailAnimation() { + return this.thumbnail_anim; + } + + /** Displays the thumbnail as a fullscreen image (used for pause preview option). + */ + public void showLastImage() { + if( MyDebug.LOG ) + Log.d(TAG, "showLastImage"); + this.show_last_image = true; + } + + public void clearLastImage() { + if( MyDebug.LOG ) + Log.d(TAG, "clearLastImage"); + this.show_last_image = false; + } + + public void cameraInOperation(boolean in_operation) { + if( in_operation && !main_activity.getPreview().isVideo() ) { + taking_picture = true; + } + else { + taking_picture = false; + front_screen_flash = false; + capture_started = false; + capture_started_time_ms = 0; + } + } + + public void turnFrontScreenFlashOn() { + if( MyDebug.LOG ) + Log.d(TAG, "turnFrontScreenFlashOn"); + front_screen_flash = true; + } + + public void onCaptureStarted() { + if( MyDebug.LOG ) + Log.d(TAG, "onCaptureStarted"); + capture_started = true; + capture_started_time_ms = System.currentTimeMillis(); + } + + public void onContinuousFocusMove(boolean start) { + if( MyDebug.LOG ) + Log.d(TAG, "onContinuousFocusMove: " + start); + if( start ) { + if( !continuous_focus_moving ) { // don't restart the animation if already in motion + continuous_focus_moving = true; + continuous_focus_moving_ms = System.currentTimeMillis(); + } + } + // if we receive start==false, we don't stop the animation - let it continue + } + + public void clearContinuousFocusMove() { + if( MyDebug.LOG ) + Log.d(TAG, "clearContinuousFocusMove"); + continuous_focus_moving = false; + continuous_focus_moving_ms = 0; + } + + private boolean getTakePhotoBorderPref() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sharedPreferences.getBoolean(PreferenceKeys.getTakePhotoBorderPreferenceKey(), true); + } + + private int getAngleHighlightColor() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + String color = sharedPreferences.getString(PreferenceKeys.getShowAngleHighlightColorPreferenceKey(), "#14e715"); + return Color.parseColor(color); + } + + private String getTimeStringFromSeconds(long time) { + int secs = (int)(time % 60); + time /= 60; + int mins = (int)(time % 60); + time /= 60; + long hours = time; + return hours + ":" + String.format(Locale.getDefault(), "%02d", mins) + ":" + String.format(Locale.getDefault(), "%02d", secs); + } + + private void drawGrids(Canvas canvas) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + Preview preview = main_activity.getPreview(); + CameraController camera_controller = preview.getCameraController(); + String preference_grid = sharedPreferences.getString(PreferenceKeys.getShowGridPreferenceKey(), "preference_grid_none"); + final float scale = getContext().getResources().getDisplayMetrics().density; + + if( camera_controller != null && preference_grid.equals("preference_grid_3x3") ) { + p.setColor(Color.WHITE); + canvas.drawLine(canvas.getWidth()/3.0f, 0.0f, canvas.getWidth()/3.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(2.0f*canvas.getWidth()/3.0f, 0.0f, 2.0f*canvas.getWidth()/3.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(0.0f, canvas.getHeight()/3.0f, canvas.getWidth()-1.0f, canvas.getHeight()/3.0f, p); + canvas.drawLine(0.0f, 2.0f*canvas.getHeight()/3.0f, canvas.getWidth()-1.0f, 2.0f*canvas.getHeight()/3.0f, p); + } + else if( camera_controller != null && preference_grid.equals("preference_grid_phi_3x3") ) { + p.setColor(Color.WHITE); + canvas.drawLine(canvas.getWidth()/2.618f, 0.0f, canvas.getWidth()/2.618f, canvas.getHeight()-1.0f, p); + canvas.drawLine(1.618f*canvas.getWidth()/2.618f, 0.0f, 1.618f*canvas.getWidth()/2.618f, canvas.getHeight()-1.0f, p); + canvas.drawLine(0.0f, canvas.getHeight()/2.618f, canvas.getWidth()-1.0f, canvas.getHeight()/2.618f, p); + canvas.drawLine(0.0f, 1.618f*canvas.getHeight()/2.618f, canvas.getWidth()-1.0f, 1.618f*canvas.getHeight()/2.618f, p); + } + else if( camera_controller != null && preference_grid.equals("preference_grid_4x2") ) { + p.setColor(Color.GRAY); + canvas.drawLine(canvas.getWidth()/4.0f, 0.0f, canvas.getWidth()/4.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(canvas.getWidth()/2.0f, 0.0f, canvas.getWidth()/2.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(3.0f*canvas.getWidth()/4.0f, 0.0f, 3.0f*canvas.getWidth()/4.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(0.0f, canvas.getHeight()/2.0f, canvas.getWidth()-1.0f, canvas.getHeight()/2.0f, p); + p.setColor(Color.WHITE); + int crosshairs_radius = (int) (20 * scale + 0.5f); // convert dps to pixels + canvas.drawLine(canvas.getWidth()/2.0f, canvas.getHeight()/2.0f - crosshairs_radius, canvas.getWidth()/2.0f, canvas.getHeight()/2.0f + crosshairs_radius, p); + canvas.drawLine(canvas.getWidth()/2.0f - crosshairs_radius, canvas.getHeight()/2.0f, canvas.getWidth()/2.0f + crosshairs_radius, canvas.getHeight()/2.0f, p); + } + else if( camera_controller != null && preference_grid.equals("preference_grid_crosshair") ) { + p.setColor(Color.WHITE); + canvas.drawLine(canvas.getWidth()/2.0f, 0.0f, canvas.getWidth()/2.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(0.0f, canvas.getHeight()/2.0f, canvas.getWidth()-1.0f, canvas.getHeight()/2.0f, p); + } + else if( camera_controller != null && ( preference_grid.equals("preference_grid_golden_spiral_right") || preference_grid.equals("preference_grid_golden_spiral_left") || preference_grid.equals("preference_grid_golden_spiral_upside_down_right") || preference_grid.equals("preference_grid_golden_spiral_upside_down_left") ) ) { + canvas.save(); + if( preference_grid.equals("preference_grid_golden_spiral_left") ) { + canvas.scale(-1.0f, 1.0f, canvas.getWidth()*0.5f, canvas.getHeight()*0.5f); + } + else if( preference_grid.equals("preference_grid_golden_spiral_right") ) { + // no transformation needed + } + else if( preference_grid.equals("preference_grid_golden_spiral_upside_down_left") ) { + canvas.rotate(180.0f, canvas.getWidth()*0.5f, canvas.getHeight()*0.5f); + } + else if( preference_grid.equals("preference_grid_golden_spiral_upside_down_right") ) { + canvas.scale(1.0f, -1.0f, canvas.getWidth()*0.5f, canvas.getHeight()*0.5f); + } + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.STROKE); + int fibb = 34; + int fibb_n = 21; + int left = 0, top = 0; + int full_width = canvas.getWidth(); + int full_height = canvas.getHeight(); + int width = (int)(full_width*((double)fibb_n)/(double)(fibb)); + int height = full_height; + + for(int count=0;count<2;count++) { + canvas.save(); + draw_rect.set(left, top, left+width, top+height); + canvas.clipRect(draw_rect); + canvas.drawRect(draw_rect, p); + draw_rect.set(left, top, left+2*width, top+2*height); + canvas.drawOval(draw_rect, p); + canvas.restore(); + + int old_fibb = fibb; + fibb = fibb_n; + fibb_n = old_fibb - fibb; + + left += width; + full_width = full_width - width; + width = full_width; + height = (int)(height*((double)fibb_n)/(double)(fibb)); + + canvas.save(); + draw_rect.set(left, top, left+width, top+height); + canvas.clipRect(draw_rect); + canvas.drawRect(draw_rect, p); + draw_rect.set(left-width, top, left+width, top+2*height); + canvas.drawOval(draw_rect, p); + canvas.restore(); + + old_fibb = fibb; + fibb = fibb_n; + fibb_n = old_fibb - fibb; + + top += height; + full_height = full_height - height; + height = full_height; + width = (int)(width*((double)fibb_n)/(double)(fibb)); + left += full_width - width; + + canvas.save(); + draw_rect.set(left, top, left+width, top+height); + canvas.clipRect(draw_rect); + canvas.drawRect(draw_rect, p); + draw_rect.set(left-width, top-height, left+width, top+height); + canvas.drawOval(draw_rect, p); + canvas.restore(); + + old_fibb = fibb; + fibb = fibb_n; + fibb_n = old_fibb - fibb; + + full_width = full_width - width; + width = full_width; + left -= width; + height = (int)(height*((double)fibb_n)/(double)(fibb)); + top += full_height - height; + + canvas.save(); + draw_rect.set(left, top, left+width, top+height); + canvas.clipRect(draw_rect); + canvas.drawRect(draw_rect, p); + draw_rect.set(left, top-height, left+2*width, top+height); + canvas.drawOval(draw_rect, p); + canvas.restore(); + + old_fibb = fibb; + fibb = fibb_n; + fibb_n = old_fibb - fibb; + + full_height = full_height - height; + height = full_height; + top -= height; + width = (int)(width*((double)fibb_n)/(double)(fibb)); + } + + canvas.restore(); + p.setStyle(Paint.Style.FILL); // reset + } + else if( camera_controller != null && ( preference_grid.equals("preference_grid_golden_triangle_1") || preference_grid.equals("preference_grid_golden_triangle_2") ) ) { + p.setColor(Color.WHITE); + double theta = Math.atan2(canvas.getWidth(), canvas.getHeight()); + double dist = canvas.getHeight() * Math.cos(theta); + float dist_x = (float)(dist * Math.sin(theta)); + float dist_y = (float)(dist * Math.cos(theta)); + if( preference_grid.equals("preference_grid_golden_triangle_1") ) { + canvas.drawLine(0.0f, canvas.getHeight()-1.0f, canvas.getWidth()-1.0f, 0.0f, p); + canvas.drawLine(0.0f, 0.0f, dist_x, canvas.getHeight()-dist_y, p); + canvas.drawLine(canvas.getWidth()-1.0f-dist_x, dist_y-1.0f, canvas.getWidth()-1.0f, canvas.getHeight()-1.0f, p); + } + else { + canvas.drawLine(0.0f, 0.0f, canvas.getWidth()-1.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(canvas.getWidth()-1.0f, 0.0f, canvas.getWidth()-1.0f-dist_x, canvas.getHeight()-dist_y, p); + canvas.drawLine(dist_x, dist_y-1.0f, 0.0f, canvas.getHeight()-1.0f, p); + } + } + else if( camera_controller != null && preference_grid.equals("preference_grid_diagonals") ) { + p.setColor(Color.WHITE); + canvas.drawLine(0.0f, 0.0f, canvas.getHeight()-1.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(canvas.getHeight()-1.0f, 0.0f, 0.0f, canvas.getHeight()-1.0f, p); + int diff = canvas.getWidth() - canvas.getHeight(); + if( diff > 0 ) { + canvas.drawLine(diff, 0.0f, diff+canvas.getHeight()-1.0f, canvas.getHeight()-1.0f, p); + canvas.drawLine(diff+canvas.getHeight()-1.0f, 0.0f, diff, canvas.getHeight()-1.0f, p); + } + } + } + + public void onDrawPreview(Canvas canvas) { + /*if( MyDebug.LOG ) + Log.d(TAG, "onDrawPreview");*/ + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getContext()); + Preview preview = main_activity.getPreview(); + CameraController camera_controller = preview.getCameraController(); + int ui_rotation = preview.getUIRotation(); + boolean has_level_angle = preview.hasLevelAngle(); + double level_angle = preview.getLevelAngle(); + boolean has_pitch_angle = preview.hasPitchAngle(); + double pitch_angle = preview.getPitchAngle(); + boolean has_geo_direction = preview.hasGeoDirection(); + double geo_direction = preview.getGeoDirection(); + boolean ui_placement_right = main_activity.getMainUI().getUIPlacementRight(); + if( main_activity.getMainUI().inImmersiveMode() ) { + String immersive_mode = sharedPreferences.getString(PreferenceKeys.getImmersiveModePreferenceKey(), "immersive_mode_low_profile"); + if( immersive_mode.equals("immersive_mode_everything") ) { + // exit, to ensure we don't display anything! + return; + } + } + final float scale = getContext().getResources().getDisplayMetrics().density; + if( camera_controller!= null && front_screen_flash ) { + p.setColor(Color.WHITE); + canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p); + } + else if( camera_controller != null && taking_picture && getTakePhotoBorderPref() ) { + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.STROKE); + float this_stroke_width = (5.0f * scale + 0.5f); // convert dps to pixels + p.setStrokeWidth(this_stroke_width); + canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p); + p.setStyle(Paint.Style.FILL); // reset + p.setStrokeWidth(stroke_width); // reset + } + drawGrids(canvas); + + if( preview.isVideo() || sharedPreferences.getString(PreferenceKeys.getPreviewSizePreferenceKey(), "preference_preview_size_wysiwyg").equals("preference_preview_size_wysiwyg") ) { + String preference_crop_guide = sharedPreferences.getString(PreferenceKeys.getShowCropGuidePreferenceKey(), "crop_guide_none"); + if( camera_controller != null && preview.getTargetRatio() > 0.0 && !preference_crop_guide.equals("crop_guide_none") ) { + p.setStyle(Paint.Style.STROKE); + p.setColor(Color.rgb(255, 235, 59)); // Yellow 500 + double crop_ratio = -1.0; + if( preference_crop_guide.equals("crop_guide_1") ) { + crop_ratio = 1.0; + } + else if( preference_crop_guide.equals("crop_guide_1.25") ) { + crop_ratio = 1.25; + } + else if( preference_crop_guide.equals("crop_guide_1.33") ) { + crop_ratio = 1.33333333; + } + else if( preference_crop_guide.equals("crop_guide_1.4") ) { + crop_ratio = 1.4; + } + else if( preference_crop_guide.equals("crop_guide_1.5") ) { + crop_ratio = 1.5; + } + else if( preference_crop_guide.equals("crop_guide_1.78") ) { + crop_ratio = 1.77777778; + } + else if( preference_crop_guide.equals("crop_guide_1.85") ) { + crop_ratio = 1.85; + } + else if( preference_crop_guide.equals("crop_guide_2.33") ) { + crop_ratio = 2.33333333; + } + else if( preference_crop_guide.equals("crop_guide_2.35") ) { + crop_ratio = 2.35006120; // actually 1920:817 + } + else if( preference_crop_guide.equals("crop_guide_2.4") ) { + crop_ratio = 2.4; + } + if( crop_ratio > 0.0 && Math.abs(preview.getTargetRatio() - crop_ratio) > 1.0e-5 ) { + /*if( MyDebug.LOG ) { + Log.d(TAG, "crop_ratio: " + crop_ratio); + Log.d(TAG, "preview_targetRatio: " + preview_targetRatio); + Log.d(TAG, "canvas width: " + canvas.getWidth()); + Log.d(TAG, "canvas height: " + canvas.getHeight()); + }*/ + int left = 1, top = 1, right = canvas.getWidth()-1, bottom = canvas.getHeight()-1; + if( crop_ratio > preview.getTargetRatio() ) { + // crop ratio is wider, so we have to crop top/bottom + double new_hheight = ((double)canvas.getWidth()) / (2.0f*crop_ratio); + top = (canvas.getHeight()/2 - (int)new_hheight); + bottom = (canvas.getHeight()/2 + (int)new_hheight); + } + else { + // crop ratio is taller, so we have to crop left/right + double new_hwidth = (((double)canvas.getHeight()) * crop_ratio) / 2.0f; + left = (canvas.getWidth()/2 - (int)new_hwidth); + right = (canvas.getWidth()/2 + (int)new_hwidth); + } + canvas.drawRect(left, top, right, bottom, p); + } + p.setStyle(Paint.Style.FILL); // reset + } + } + + if( show_last_image && last_thumbnail != null ) { + // If changing this code, ensure that pause preview still works when: + // - Taking a photo in portrait or landscape - and check rotating the device while preview paused + // - Taking a photo with lock to portrait/landscape options still shows the thumbnail with aspect ratio preserved + p.setColor(Color.rgb(0, 0, 0)); // in case image doesn't cover the canvas (due to different aspect ratios) + canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p); // in case + last_image_src_rect.left = 0; + last_image_src_rect.top = 0; + last_image_src_rect.right = last_thumbnail.getWidth(); + last_image_src_rect.bottom = last_thumbnail.getHeight(); + if( ui_rotation == 90 || ui_rotation == 270 ) { + last_image_src_rect.right = last_thumbnail.getHeight(); + last_image_src_rect.bottom = last_thumbnail.getWidth(); + } + last_image_dst_rect.left = 0; + last_image_dst_rect.top = 0; + last_image_dst_rect.right = canvas.getWidth(); + last_image_dst_rect.bottom = canvas.getHeight(); + /*if( MyDebug.LOG ) { + Log.d(TAG, "thumbnail: " + last_thumbnail.getWidth() + " x " + last_thumbnail.getHeight()); + Log.d(TAG, "canvas: " + canvas.getWidth() + " x " + canvas.getHeight()); + }*/ + last_image_matrix.setRectToRect(last_image_src_rect, last_image_dst_rect, Matrix.ScaleToFit.CENTER); // use CENTER to preserve aspect ratio + if( ui_rotation == 90 || ui_rotation == 270 ) { + // the rotation maps (0, 0) to (tw/2 - th/2, th/2 - tw/2), so we translate to undo this + float diff = last_thumbnail.getHeight() - last_thumbnail.getWidth(); + last_image_matrix.preTranslate(diff/2.0f, -diff/2.0f); + } + last_image_matrix.preRotate(ui_rotation, last_thumbnail.getWidth()/2.0f, last_thumbnail.getHeight()/2.0f); + canvas.drawBitmap(last_thumbnail, last_image_matrix, p); + } + + // note, no need to check preferences here, as we do that when setting thumbnail_anim + if( camera_controller != null && this.thumbnail_anim && last_thumbnail != null ) { + long time = System.currentTimeMillis() - this.thumbnail_anim_start_ms; + final long duration = 500; + if( time > duration ) { + if( MyDebug.LOG ) + Log.d(TAG, "thumbnail_anim finished"); + this.thumbnail_anim = false; + } + else { + thumbnail_anim_src_rect.left = 0; + thumbnail_anim_src_rect.top = 0; + thumbnail_anim_src_rect.right = last_thumbnail.getWidth(); + thumbnail_anim_src_rect.bottom = last_thumbnail.getHeight(); + View galleryButton = main_activity.findViewById(R.id.gallery); + float alpha = ((float)time)/(float)duration; + + int st_x = canvas.getWidth()/2; + int st_y = canvas.getHeight()/2; + int nd_x = galleryButton.getLeft() + galleryButton.getWidth()/2; + int nd_y = galleryButton.getTop() + galleryButton.getHeight()/2; + int thumbnail_x = (int)( (1.0f-alpha)*st_x + alpha*nd_x ); + int thumbnail_y = (int)( (1.0f-alpha)*st_y + alpha*nd_y ); + + float st_w = canvas.getWidth(); + float st_h = canvas.getHeight(); + float nd_w = galleryButton.getWidth(); + float nd_h = galleryButton.getHeight(); + //int thumbnail_w = (int)( (1.0f-alpha)*st_w + alpha*nd_w ); + //int thumbnail_h = (int)( (1.0f-alpha)*st_h + alpha*nd_h ); + float correction_w = st_w/nd_w - 1.0f; + float correction_h = st_h/nd_h - 1.0f; + int thumbnail_w = (int)(st_w/(1.0f+alpha*correction_w)); + int thumbnail_h = (int)(st_h/(1.0f+alpha*correction_h)); + thumbnail_anim_dst_rect.left = thumbnail_x - thumbnail_w/2; + thumbnail_anim_dst_rect.top = thumbnail_y - thumbnail_h/2; + thumbnail_anim_dst_rect.right = thumbnail_x + thumbnail_w/2; + thumbnail_anim_dst_rect.bottom = thumbnail_y + thumbnail_h/2; + //canvas.drawBitmap(this.thumbnail, thumbnail_anim_src_rect, thumbnail_anim_dst_rect, p); + thumbnail_anim_matrix.setRectToRect(thumbnail_anim_src_rect, thumbnail_anim_dst_rect, Matrix.ScaleToFit.FILL); + //thumbnail_anim_matrix.reset(); + if( ui_rotation == 90 || ui_rotation == 270 ) { + float ratio = ((float)last_thumbnail.getWidth())/(float)last_thumbnail.getHeight(); + thumbnail_anim_matrix.preScale(ratio, 1.0f/ratio, last_thumbnail.getWidth()/2.0f, last_thumbnail.getHeight()/2.0f); + } + thumbnail_anim_matrix.preRotate(ui_rotation, last_thumbnail.getWidth()/2.0f, last_thumbnail.getHeight()/2.0f); + canvas.drawBitmap(last_thumbnail, thumbnail_anim_matrix, p); + } + } + + canvas.save(); + canvas.rotate(ui_rotation, canvas.getWidth()/2.0f, canvas.getHeight()/2.0f); + + int text_y = (int) (20 * scale + 0.5f); // convert dps to pixels + // fine tuning to adjust placement of text with respect to the GUI, depending on orientation + int text_base_y = 0; + if( ui_rotation == ( ui_placement_right ? 0 : 180 ) ) { + text_base_y = canvas.getHeight() - (int)(0.5*text_y); + } + else if( ui_rotation == ( ui_placement_right ? 180 : 0 ) ) { + text_base_y = canvas.getHeight() - (int)(2.5*text_y); // leave room for GUI icons + } + else if( ui_rotation == 90 || ui_rotation == 270 ) { + //text_base_y = canvas.getHeight() + (int)(0.5*text_y); + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + // align with "top" of the take_photo button, but remember to take the rotation into account! + view.getLocationOnScreen(gui_location); + int view_left = gui_location[0]; + preview.getView().getLocationOnScreen(gui_location); + int this_left = gui_location[0]; + int diff_x = view_left - ( this_left + canvas.getWidth()/2 ); + /*if( MyDebug.LOG ) { + Log.d(TAG, "view left: " + view_left); + Log.d(TAG, "this left: " + this_left); + Log.d(TAG, "canvas is " + canvas.getWidth() + " x " + canvas.getHeight()); + }*/ + int max_x = canvas.getWidth(); + if( ui_rotation == 90 ) { + // so we don't interfere with the top bar info (datetime, free memory, ISO) + max_x -= (int)(2.5*text_y); + } + if( canvas.getWidth()/2 + diff_x > max_x ) { + // in case goes off the size of the canvas, for "black bar" cases (when preview aspect ratio != screen aspect ratio) + diff_x = max_x - canvas.getWidth()/2; + } + text_base_y = canvas.getHeight()/2 + diff_x - (int)(0.5*text_y); + } + final int top_y = (int) (5 * scale + 0.5f); // convert dps to pixels + final int location_size = (int) (20 * scale + 0.5f); // convert dps to pixels + + final String ybounds_text = getContext().getResources().getString(R.string.zoom) + getContext().getResources().getString(R.string.angle) + getContext().getResources().getString(R.string.direction); + final double close_angle = 1.0f; + if( camera_controller != null && !preview.isPreviewPaused() ) { + /*canvas.drawText("PREVIEW", canvas.getWidth() / 2, + canvas.getHeight() / 2, p);*/ + boolean draw_angle = has_level_angle && sharedPreferences.getBoolean(PreferenceKeys.getShowAnglePreferenceKey(), true); + boolean draw_geo_direction = has_geo_direction && sharedPreferences.getBoolean(PreferenceKeys.getShowGeoDirectionPreferenceKey(), false); + if( draw_angle ) { + int color = Color.WHITE; + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + int pixels_offset_x = 0; + if( draw_geo_direction ) { + pixels_offset_x = - (int) (82 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.LEFT); + } + else { + p.setTextAlign(Paint.Align.CENTER); + } + if( Math.abs(level_angle) <= close_angle ) { + color = getAngleHighlightColor(); + p.setUnderlineText(true); + } + String number_string = decimalFormat.format(level_angle); + number_string = number_string.replaceAll( "^-(?=0(.0*)?$)", ""); // avoids displaying "-0.0", see http://stackoverflow.com/questions/11929096/negative-sign-in-case-of-zero-in-java + String string = getContext().getResources().getString(R.string.angle) + ": " + number_string + (char)0x00B0; + applicationInterface.drawTextWithBackground(canvas, p, string, color, Color.BLACK, canvas.getWidth() / 2 + pixels_offset_x, text_base_y, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, ybounds_text, true); + p.setUnderlineText(false); + } + if( draw_geo_direction ) { + int color = Color.WHITE; + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + if( draw_angle ) { + p.setTextAlign(Paint.Align.LEFT); + } + else { + p.setTextAlign(Paint.Align.CENTER); + } + float geo_angle = (float)Math.toDegrees(geo_direction); + if( geo_angle < 0.0f ) { + geo_angle += 360.0f; + } + String string = " " + getContext().getResources().getString(R.string.direction) + ": " + Math.round(geo_angle) + (char)0x00B0; + applicationInterface.drawTextWithBackground(canvas, p, string, color, Color.BLACK, canvas.getWidth() / 2, text_base_y, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, ybounds_text, true); + } + if( preview.isOnTimer() ) { + long remaining_time = (preview.getTimerEndTime() - System.currentTimeMillis() + 999)/1000; + if( MyDebug.LOG ) + Log.d(TAG, "remaining_time: " + remaining_time); + if( remaining_time > 0 ) { + p.setTextSize(42 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.CENTER); + String time_s; + if( remaining_time < 60 ) { + // simpler to just show seconds when less than a minute + time_s = "" + remaining_time; + } + else { + time_s = getTimeStringFromSeconds(remaining_time); + } + applicationInterface.drawTextWithBackground(canvas, p, time_s, Color.rgb(244, 67, 54), Color.BLACK, canvas.getWidth() / 2, canvas.getHeight() / 2); // Red 500 + } + } + else if( preview.isVideoRecording() ) { + long video_time = preview.getVideoTime(); + String time_s = getTimeStringFromSeconds(video_time/1000); + /*if( MyDebug.LOG ) + Log.d(TAG, "video_time: " + video_time + " " + time_s);*/ + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.CENTER); + int pixels_offset_y = 3*text_y; // avoid overwriting the zoom, and also allow a bit extra space + int color = Color.rgb(244, 67, 54); // Red 500 + if( main_activity.isScreenLocked() ) { + // writing in reverse order, bottom to top + applicationInterface.drawTextWithBackground(canvas, p, getContext().getResources().getString(R.string.screen_lock_message_2), color, Color.BLACK, canvas.getWidth() / 2, text_base_y - pixels_offset_y); + pixels_offset_y += text_y; + applicationInterface.drawTextWithBackground(canvas, p, getContext().getResources().getString(R.string.screen_lock_message_1), color, Color.BLACK, canvas.getWidth() / 2, text_base_y - pixels_offset_y); + pixels_offset_y += text_y; + } + if( !preview.isVideoRecordingPaused() || ((int)(System.currentTimeMillis() / 500)) % 2 == 0 ) { // if video is paused, then flash the video time + applicationInterface.drawTextWithBackground(canvas, p, time_s, color, Color.BLACK, canvas.getWidth() / 2, text_base_y - pixels_offset_y); + } + } + else if( taking_picture && capture_started ) { + if( camera_controller.isManualISO() ) { + // only show "capturing" text with time for manual exposure time >= 0.5s + long exposure_time = camera_controller.getExposureTime(); + if( exposure_time >= 500000000L ) { + long time_ms = System.currentTimeMillis() - capture_started_time_ms; + if( ((int)(System.currentTimeMillis() / 500)) % 2 == 0 ) { + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.CENTER); + int pixels_offset_y = 3*text_y; // avoid overwriting the zoom, and also allow a bit extra space + int color = Color.rgb(244, 67, 54); // Red 500 + applicationInterface.drawTextWithBackground(canvas, p, getContext().getResources().getString(R.string.capturing), color, Color.BLACK, canvas.getWidth() / 2, text_base_y - pixels_offset_y); + } + } + } + } + } + else if( camera_controller == null ) { + /*if( MyDebug.LOG ) { + Log.d(TAG, "no camera!"); + Log.d(TAG, "width " + canvas.getWidth() + " height " + canvas.getHeight()); + }*/ + p.setColor(Color.WHITE); + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.CENTER); + int pixels_offset = (int) (20 * scale + 0.5f); // convert dps to pixels + if( preview.hasPermissions() ) { + canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_1), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f, p); + canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_2), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f + pixels_offset, p); + canvas.drawText(getContext().getResources().getString(R.string.failed_to_open_camera_3), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f + 2*pixels_offset, p); + } + else { + canvas.drawText(getContext().getResources().getString(R.string.no_permission), canvas.getWidth() / 2.0f, canvas.getHeight() / 2.0f, p); + } + //canvas.drawRect(0.0f, 0.0f, 100.0f, 100.0f, p); + //canvas.drawRGB(255, 0, 0); + //canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p); + } + if( camera_controller != null && sharedPreferences.getBoolean(PreferenceKeys.getShowISOPreferenceKey(), true) ) { + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.LEFT); + int location_x = (int) (50 * scale + 0.5f); // convert dps to pixels + int location_y = top_y + (int) (32 * scale + 0.5f); // convert dps to pixels + //int location_y2 = top_y + (int) (48 * scale + 0.5f); // convert dps to pixels + if( ui_rotation == 90 || ui_rotation == 270 ) { + int diff = canvas.getWidth() - canvas.getHeight(); + location_x += diff/2; + location_y -= diff/2; + //location_y2 -= diff/2; + } + if( ui_rotation == 90 ) { + location_y = canvas.getHeight() - location_y - location_size; + //location_y2 = canvas.getHeight() - location_y2 - location_size; + } + if( ui_rotation == 180 ) { + location_x = canvas.getWidth() - location_x; + p.setTextAlign(Paint.Align.RIGHT); + } + String string = ""; + if( camera_controller.captureResultHasIso() ) { + int iso = camera_controller.captureResultIso(); + if( string.length() > 0 ) + string += " "; + string += preview.getISOString(iso); + } + if( camera_controller.captureResultHasExposureTime() ) { + long exposure_time = camera_controller.captureResultExposureTime(); + if( string.length() > 0 ) + string += " "; + string += preview.getExposureTimeString(exposure_time); + } + /*if( camera_controller.captureResultHasFrameDuration() ) { + long frame_duration = camera_controller.captureResultFrameDuration(); + if( string.length() > 0 ) + string += " "; + string += preview.getFrameDurationString(frame_duration); + }*/ + if( string.length() > 0 ) { + boolean is_scanning = false; + if( camera_controller.captureResultIsAEScanning() ) { + // only show as scanning if in auto ISO mode (problem on Nexus 6 at least that if we're in manual ISO mode, after pausing and + // resuming, the camera driver continually reports CONTROL_AE_STATE_SEARCHING) + String value = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), main_activity.getPreview().getCameraController().getDefaultISO()); + if( value.equals("auto") ) { + is_scanning = true; + } + } + + int text_color = Color.rgb(255, 235, 59); // Yellow 500 + if( is_scanning ) { + // we only change the color if ae scanning is at least a certain time, otherwise we get a lot of flickering of the color + if( ae_started_scanning_ms == -1 ) { + ae_started_scanning_ms = System.currentTimeMillis(); + } + else if( System.currentTimeMillis() - ae_started_scanning_ms > 500 ) { + text_color = Color.rgb(244, 67, 54); // Red 500 + } + } + else { + ae_started_scanning_ms = -1; + } + applicationInterface.drawTextWithBackground(canvas, p, string, text_color, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP, ybounds_text, true); + } + /*if( camera_controller.captureResultHasFocusDistance() ) { + float dist_min = camera_controller.captureResultFocusDistanceMin(); + float dist_max = camera_controller.captureResultFocusDistanceMin(); + string = preview.getFocusDistanceString(dist_min, dist_max); + applicationInterface.drawTextWithBackground(canvas, p, string, Color.rgb(255, 235, 59), Color.BLACK, location_x, location_y2, MyApplicationInterface.Alignment.ALIGNMENT_TOP, ybounds_text, true); // Yellow 500 + }*/ + } + if( preview.supportsZoom() && camera_controller != null && sharedPreferences.getBoolean(PreferenceKeys.getShowZoomPreferenceKey(), true) ) { + float zoom_ratio = preview.getZoomRatio(); + // only show when actually zoomed in + if( zoom_ratio > 1.0f + 1.0e-5f ) { + // Convert the dps to pixels, based on density scale + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.CENTER); + applicationInterface.drawTextWithBackground(canvas, p, getContext().getResources().getString(R.string.zoom) + ": " + zoom_ratio +"x", Color.WHITE, Color.BLACK, canvas.getWidth() / 2, text_base_y - text_y, MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM, ybounds_text, true); + } + } + + if( sharedPreferences.getBoolean(PreferenceKeys.getShowBatteryPreferenceKey(), true) ) { + if( !this.has_battery_frac || System.currentTimeMillis() > this.last_battery_time + 60000 ) { + // only check periodically - unclear if checking is costly in any way + // note that it's fine to call registerReceiver repeatedly - we pass a null receiver, so this is fine as a "one shot" use + Intent batteryStatus = main_activity.registerReceiver(null, battery_ifilter); + int battery_level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int battery_scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + has_battery_frac = true; + battery_frac = battery_level/(float)battery_scale; + last_battery_time = System.currentTimeMillis(); + if( MyDebug.LOG ) + Log.d(TAG, "Battery status is " + battery_level + " / " + battery_scale + " : " + battery_frac); + } + //battery_frac = 0.2999f; // test + int battery_x = (int) (5 * scale + 0.5f); // convert dps to pixels + int battery_y = top_y; + int battery_width = (int) (5 * scale + 0.5f); // convert dps to pixels + int battery_height = 4*battery_width; + if( ui_rotation == 90 || ui_rotation == 270 ) { + int diff = canvas.getWidth() - canvas.getHeight(); + battery_x += diff/2; + battery_y -= diff/2; + } + if( ui_rotation == 90 ) { + battery_y = canvas.getHeight() - battery_y - battery_height; + } + if( ui_rotation == 180 ) { + battery_x = canvas.getWidth() - battery_x - battery_width; + } + boolean draw_battery = true; + if( battery_frac <= 0.05f ) { + // flash icon at this low level + draw_battery = ((( System.currentTimeMillis() / 1000 )) % 2) == 0; + } + if( draw_battery ) { + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.STROKE); + canvas.drawRect(battery_x, battery_y, battery_x+battery_width, battery_y+battery_height, p); + p.setColor(battery_frac > 0.15f ? Color.rgb(37, 155, 36) : Color.rgb(244, 67, 54)); // Green 500 or Red 500 + p.setStyle(Paint.Style.FILL); + canvas.drawRect(battery_x+1, battery_y+1+(1.0f-battery_frac)*(battery_height-2), battery_x+battery_width-1, battery_y+battery_height-1, p); + } + } + + boolean store_location = sharedPreferences.getBoolean(PreferenceKeys.getLocationPreferenceKey(), false); + if( store_location ) { + int location_x = (int) (20 * scale + 0.5f); // convert dps to pixels + int location_y = top_y; + if( ui_rotation == 90 || ui_rotation == 270 ) { + int diff = canvas.getWidth() - canvas.getHeight(); + location_x += diff/2; + location_y -= diff/2; + } + if( ui_rotation == 90 ) { + location_y = canvas.getHeight() - location_y - location_size; + } + if( ui_rotation == 180 ) { + location_x = canvas.getWidth() - location_x - location_size; + } + location_dest.set(location_x, location_y, location_x + location_size, location_y + location_size); + if( applicationInterface.getLocation() != null ) { + canvas.drawBitmap(location_bitmap, null, location_dest, p); + int location_radius = location_size/10; + int indicator_x = location_x + location_size; + int indicator_y = location_y + location_radius/2 + 1; + p.setStyle(Paint.Style.FILL); + p.setColor(applicationInterface.getLocation().getAccuracy() < 25.01f ? Color.rgb(37, 155, 36) : Color.rgb(255, 235, 59)); // Green 500 or Yellow 500 + canvas.drawCircle(indicator_x, indicator_y, location_radius, p); + } + else { + canvas.drawBitmap(location_off_bitmap, null, location_dest, p); + } + } + + if( sharedPreferences.getBoolean(PreferenceKeys.getShowTimePreferenceKey(), true) ) { + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.LEFT); + int location_x = (int) (50 * scale + 0.5f); // convert dps to pixels + int location_y = top_y; + if( ui_rotation == 90 || ui_rotation == 270 ) { + int diff = canvas.getWidth() - canvas.getHeight(); + location_x += diff/2; + location_y -= diff/2; + } + if( ui_rotation == 90 ) { + location_y = canvas.getHeight() - location_y - location_size; + } + if( ui_rotation == 180 ) { + location_x = canvas.getWidth() - location_x; + p.setTextAlign(Paint.Align.RIGHT); + } + Calendar c = Calendar.getInstance(); + // n.b., DateFormat.getTimeInstance() ignores user preferences such as 12/24 hour or date format, but this is an Android bug. + // Whilst DateUtils.formatDateTime doesn't have that problem, it doesn't print out seconds! See: + // http://stackoverflow.com/questions/15981516/simpledateformat-gettimeinstance-ignores-24-hour-format + // http://daniel-codes.blogspot.co.uk/2013/06/how-to-correctly-format-datetime.html + // http://code.google.com/p/android/issues/detail?id=42104 + // also possibly related https://code.google.com/p/android/issues/detail?id=181201 + String current_time = DateFormat.getTimeInstance().format(c.getTime()); + //String current_time = DateUtils.formatDateTime(getContext(), c.getTimeInMillis(), DateUtils.FORMAT_SHOW_TIME); + applicationInterface.drawTextWithBackground(canvas, p, current_time, Color.WHITE, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP); + } + + if( camera_controller != null && sharedPreferences.getBoolean(PreferenceKeys.getShowFreeMemoryPreferenceKey(), true) ) { + p.setTextSize(14 * scale + 0.5f); // convert dps to pixels + p.setTextAlign(Paint.Align.LEFT); + int location_x = (int) (50 * scale + 0.5f); // convert dps to pixels + int location_y = top_y + (int) (16 * scale + 0.5f); // convert dps to pixels + if( ui_rotation == 90 || ui_rotation == 270 ) { + int diff = canvas.getWidth() - canvas.getHeight(); + location_x += diff/2; + location_y -= diff/2; + } + if( ui_rotation == 90 ) { + location_y = canvas.getHeight() - location_y - location_size; + } + if( ui_rotation == 180 ) { + location_x = canvas.getWidth() - location_x; + p.setTextAlign(Paint.Align.RIGHT); + } + long time_now = System.currentTimeMillis(); + if( last_free_memory_time == 0 || time_now > last_free_memory_time + 1000 ) { + long free_mb = main_activity.freeMemory(); + if( free_mb >= 0 ) { + free_memory_gb = free_mb/1024.0f; + } + last_free_memory_time = time_now; // always set this, so that in case of free memory not being available, we aren't calling freeMemory() every frame + } + if( free_memory_gb >= 0.0f ) { + applicationInterface.drawTextWithBackground(canvas, p, getContext().getResources().getString(R.string.free_memory) + ": " + decimalFormat.format(free_memory_gb) + getContext().getResources().getString(R.string.gb_abbreviation), Color.WHITE, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP); + } + } + + canvas.restore(); + + boolean show_angle_line = sharedPreferences.getBoolean(PreferenceKeys.getShowAngleLinePreferenceKey(), false); + boolean show_pitch_lines = sharedPreferences.getBoolean(PreferenceKeys.getShowPitchLinesPreferenceKey(), false); + boolean show_geo_direction_lines = sharedPreferences.getBoolean(PreferenceKeys.getShowGeoDirectionLinesPreferenceKey(), false); + if( camera_controller != null && !preview.isPreviewPaused() && has_level_angle && ( show_angle_line || show_pitch_lines || show_geo_direction_lines ) ) { + // n.b., must draw this without the standard canvas rotation + int radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 60 : 80; + int radius = (int) (radius_dps * scale + 0.5f); // convert dps to pixels + double angle = - preview.getOrigLevelAngle(); + // see http://android-developers.blogspot.co.uk/2010/09/one-screen-turn-deserves-another.html + int rotation = main_activity.getWindowManager().getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_90: + case Surface.ROTATION_270: + angle -= 90.0; + break; + case Surface.ROTATION_0: + case Surface.ROTATION_180: + default: + break; + } + /*if( MyDebug.LOG ) { + Log.d(TAG, "orig_level_angle: " + preview.getOrigLevelAngle()); + Log.d(TAG, "angle: " + angle); + }*/ + int cx = canvas.getWidth()/2; + int cy = canvas.getHeight()/2; + + boolean is_level = false; + if( Math.abs(level_angle) <= close_angle ) { // n.b., use level_angle, not angle or orig_level_angle + is_level = true; + } + + if( is_level ) { + radius = (int)(radius * 1.2); + } + + canvas.save(); + canvas.rotate((float)angle, cx, cy); + + final int line_alpha = 96; + float hthickness = (0.5f * scale + 0.5f); // convert dps to pixels + p.setStyle(Paint.Style.FILL); + if( show_angle_line ) { + // draw outline + p.setColor(Color.BLACK); + p.setAlpha(64); + // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21 + draw_rect.set(cx - radius - hthickness, cy - 2 * hthickness, cx + radius + hthickness, cy + 2 * hthickness); + canvas.drawRoundRect(draw_rect, 2 * hthickness, 2 * hthickness, p); + // draw the vertical crossbar + draw_rect.set(cx - 2 * hthickness, cy - radius / 2 - hthickness, cx + 2 * hthickness, cy + radius / 2 + hthickness); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + // draw inner portion + if (is_level) { + p.setColor(getAngleHighlightColor()); + } else { + p.setColor(Color.WHITE); + } + p.setAlpha(line_alpha); + draw_rect.set(cx - radius, cy - hthickness, cx + radius, cy + hthickness); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + + // draw the vertical crossbar + draw_rect.set(cx - hthickness, cy - radius / 2, cx + hthickness, cy + radius / 2); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + + if (is_level) { + // draw a second line + + p.setColor(Color.BLACK); + p.setAlpha(64); + draw_rect.set(cx - radius - hthickness, cy - 7 * hthickness, cx + radius + hthickness, cy - 3 * hthickness); + canvas.drawRoundRect(draw_rect, 2 * hthickness, 2 * hthickness, p); + + p.setColor(getAngleHighlightColor()); + p.setAlpha(line_alpha); + draw_rect.set(cx - radius, cy - 6 * hthickness, cx + radius, cy - 4 * hthickness); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + } + } + float camera_angle_x = preview.getViewAngleX(); + float camera_angle_y = preview.getViewAngleY(); + float angle_scale_x = (float)( canvas.getWidth() / (2.0 * Math.tan( Math.toRadians((camera_angle_x/2.0)) )) ); + float angle_scale_y = (float)( canvas.getHeight() / (2.0 * Math.tan( Math.toRadians((camera_angle_y/2.0)) )) ); + /*if( MyDebug.LOG ) { + Log.d(TAG, "camera_angle_x: " + camera_angle_x); + Log.d(TAG, "camera_angle_y: " + camera_angle_y); + Log.d(TAG, "angle_scale_x: " + angle_scale_x); + Log.d(TAG, "angle_scale_y: " + angle_scale_y); + Log.d(TAG, "angle_scale_x/scale: " + angle_scale_x/scale); + Log.d(TAG, "angle_scale_y/scale: " + angle_scale_y/scale); + }*/ + /*if( MyDebug.LOG ) { + Log.d(TAG, "has_pitch_angle?: " + has_pitch_angle); + Log.d(TAG, "show_pitch_lines?: " + show_pitch_lines); + }*/ + float angle_scale = (float)Math.sqrt( angle_scale_x*angle_scale_x + angle_scale_y*angle_scale_y ); + angle_scale *= preview.getZoomRatio(); + if( has_pitch_angle && show_pitch_lines ) { + int pitch_radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 100 : 80; + int pitch_radius = (int) (pitch_radius_dps * scale + 0.5f); // convert dps to pixels + int angle_step = 10; + if( preview.getZoomRatio() >= 2.0f ) + angle_step = 5; + for(int latitude_angle=-90;latitude_angle<=90;latitude_angle+=angle_step) { + double this_angle = pitch_angle - latitude_angle; + if( Math.abs(this_angle) < 90.0 ) { + float pitch_distance = angle_scale * (float)Math.tan( Math.toRadians(this_angle) ); // angle_scale is already in pixels rather than dps + /*if( MyDebug.LOG ) { + Log.d(TAG, "pitch_angle: " + pitch_angle); + Log.d(TAG, "pitch_distance_dp: " + pitch_distance_dp); + }*/ + // draw outline + p.setColor(Color.BLACK); + p.setAlpha(64); + // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21 + draw_rect.set(cx - pitch_radius - hthickness, cy + pitch_distance - 2*hthickness, cx + pitch_radius + hthickness, cy + pitch_distance + 2*hthickness); + canvas.drawRoundRect(draw_rect, 2*hthickness, 2*hthickness, p); + // draw inner portion + p.setColor(Color.WHITE); + p.setTextAlign(Paint.Align.LEFT); + if( latitude_angle == 0 && Math.abs(pitch_angle) < 1.0 ) { + p.setAlpha(255); + } + else { + p.setAlpha(line_alpha); + } + draw_rect.set(cx - pitch_radius, cy + pitch_distance - hthickness, cx + pitch_radius, cy + pitch_distance + hthickness); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + // draw pitch angle indicator + applicationInterface.drawTextWithBackground(canvas, p, "" + latitude_angle + "\u00B0", p.getColor(), Color.BLACK, (int)(cx + pitch_radius + 4*hthickness), (int)(cy + pitch_distance - 2*hthickness), MyApplicationInterface.Alignment.ALIGNMENT_CENTRE); + } + } + } + if( has_geo_direction && has_pitch_angle && show_geo_direction_lines ) { + int geo_radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 80 : 100; + int geo_radius = (int) (geo_radius_dps * scale + 0.5f); // convert dps to pixels + float geo_angle = (float)Math.toDegrees(geo_direction); + int angle_step = 10; + if( preview.getZoomRatio() >= 2.0f ) + angle_step = 5; + for(int longitude_angle=0;longitude_angle<360;longitude_angle+=angle_step) { + double this_angle = longitude_angle - geo_angle; + /*if( MyDebug.LOG ) { + Log.d(TAG, "longitude_angle: " + longitude_angle); + Log.d(TAG, "geo_angle: " + geo_angle); + Log.d(TAG, "this_angle: " + this_angle); + }*/ + // normalise to be in interval [0, 360) + while( this_angle >= 360.0 ) + this_angle -= 360.0; + while( this_angle < -360.0 ) + this_angle += 360.0; + // pick shortest angle + if( this_angle > 180.0 ) + this_angle = - (360.0 - this_angle); + if( Math.abs(this_angle) < 90.0 ) { + /*if( MyDebug.LOG ) { + Log.d(TAG, "this_angle is now: " + this_angle); + }*/ + float geo_distance = angle_scale * (float)Math.tan( Math.toRadians(this_angle) ); // angle_scale is already in pixels rather than dps + // draw outline + p.setColor(Color.BLACK); + p.setAlpha(64); + // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21 + draw_rect.set(cx + geo_distance - 2*hthickness, cy - geo_radius - hthickness, cx + geo_distance + 2*hthickness, cy + geo_radius + hthickness); + canvas.drawRoundRect(draw_rect, 2*hthickness, 2*hthickness, p); + // draw inner portion + p.setColor(Color.WHITE); + p.setTextAlign(Paint.Align.CENTER); + p.setAlpha(line_alpha); + draw_rect.set(cx + geo_distance - hthickness, cy - geo_radius, cx + geo_distance + hthickness, cy + geo_radius); + canvas.drawRoundRect(draw_rect, hthickness, hthickness, p); + // draw geo direction angle indicator + applicationInterface.drawTextWithBackground(canvas, p, "" + longitude_angle + "\u00B0", p.getColor(), Color.BLACK, (int)(cx + geo_distance), (int)(cy - geo_radius - 4*hthickness), MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM); + } + } + } + + p.setAlpha(255); + p.setStyle(Paint.Style.FILL); // reset + + canvas.restore(); + } + + if( camera_controller != null && continuous_focus_moving ) { + long dt = System.currentTimeMillis() - continuous_focus_moving_ms; + final long length = 1000; + if( dt <= length ) { + float frac = ((float)dt) / (float)length; + float pos_x = canvas.getWidth()/2.0f; + float pos_y = canvas.getHeight()/2.0f; + float min_radius = (40 * scale + 0.5f); // convert dps to pixels + float max_radius = (60 * scale + 0.5f); // convert dps to pixels + float radius; + if( frac < 0.5f ) { + float alpha = frac*2.0f; + radius = (1.0f-alpha) * min_radius + alpha * max_radius; + } + else { + float alpha = (frac-0.5f)*2.0f; + radius = (1.0f-alpha) * max_radius + alpha * min_radius; + } + /*if( MyDebug.LOG ) { + Log.d(TAG, "dt: " + dt); + Log.d(TAG, "radius: " + radius); + }*/ + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.STROKE); + canvas.drawCircle(pos_x, pos_y, radius, p); + p.setStyle(Paint.Style.FILL); // reset + } + else { + continuous_focus_moving = false; + } + } + + if( preview.isFocusWaiting() || preview.isFocusRecentSuccess() || preview.isFocusRecentFailure() ) { + long time_since_focus_started = preview.timeSinceStartedAutoFocus(); + float min_radius = (40 * scale + 0.5f); // convert dps to pixels + float max_radius = (45 * scale + 0.5f); // convert dps to pixels + float radius = min_radius; + if( time_since_focus_started > 0 ) { + final long length = 500; + float frac = ((float)time_since_focus_started) / (float)length; + if( frac > 1.0f ) + frac = 1.0f; + if( frac < 0.5f ) { + float alpha = frac*2.0f; + radius = (1.0f-alpha) * min_radius + alpha * max_radius; + } + else { + float alpha = (frac-0.5f)*2.0f; + radius = (1.0f-alpha) * max_radius + alpha * min_radius; + } + } + int size = (int)radius; + + if( preview.isFocusRecentSuccess() ) + p.setColor(Color.rgb(20, 231, 21)); // Green A400 + else if( preview.isFocusRecentFailure() ) + p.setColor(Color.rgb(244, 67, 54)); // Red 500 + else + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.STROKE); + int pos_x; + int pos_y; + if( preview.hasFocusArea() ) { + Pair focus_pos = preview.getFocusPos(); + pos_x = focus_pos.first; + pos_y = focus_pos.second; + } + else { + pos_x = canvas.getWidth() / 2; + pos_y = canvas.getHeight() / 2; + } + float frac = 0.5f; + // horizontal strokes + canvas.drawLine(pos_x - size, pos_y - size, pos_x - frac*size, pos_y - size, p); + canvas.drawLine(pos_x + frac*size, pos_y - size, pos_x + size, pos_y - size, p); + canvas.drawLine(pos_x - size, pos_y + size, pos_x - frac*size, pos_y + size, p); + canvas.drawLine(pos_x + frac*size, pos_y + size, pos_x + size, pos_y + size, p); + // vertical strokes + canvas.drawLine(pos_x - size, pos_y - size, pos_x - size, pos_y - frac*size, p); + canvas.drawLine(pos_x - size, pos_y + frac*size, pos_x - size, pos_y + size, p); + canvas.drawLine(pos_x + size, pos_y - size, pos_x + size, pos_y - frac*size, p); + canvas.drawLine(pos_x + size, pos_y + frac*size, pos_x + size, pos_y + size, p); + p.setStyle(Paint.Style.FILL); // reset + } + + CameraController.Face [] faces_detected = preview.getFacesDetected(); + if( faces_detected != null ) { + p.setColor(Color.rgb(255, 235, 59)); // Yellow 500 + p.setStyle(Paint.Style.STROKE); + for(CameraController.Face face : faces_detected) { + // Android doc recommends filtering out faces with score less than 50 (same for both Camera and Camera2 APIs) + if( face.score >= 50 ) { + face_rect.set(face.rect); + preview.getCameraToPreviewMatrix().mapRect(face_rect); + /*int eye_radius = (int) (5 * scale + 0.5f); // convert dps to pixels + int mouth_radius = (int) (10 * scale + 0.5f); // convert dps to pixels + float [] top_left = {face.rect.left, face.rect.top}; + float [] bottom_right = {face.rect.right, face.rect.bottom}; + canvas.drawRect(top_left[0], top_left[1], bottom_right[0], bottom_right[1], p);*/ + canvas.drawRect(face_rect, p); + /*if( face.leftEye != null ) { + float [] left_point = {face.leftEye.x, face.leftEye.y}; + cameraToPreview(left_point); + canvas.drawCircle(left_point[0], left_point[1], eye_radius, p); + } + if( face.rightEye != null ) { + float [] right_point = {face.rightEye.x, face.rightEye.y}; + cameraToPreview(right_point); + canvas.drawCircle(right_point[0], right_point[1], eye_radius, p); + } + if( face.mouth != null ) { + float [] mouth_point = {face.mouth.x, face.mouth.y}; + cameraToPreview(mouth_point); + canvas.drawCircle(mouth_point[0], mouth_point[1], mouth_radius, p); + }*/ + } + } + p.setStyle(Paint.Style.FILL); // reset + } + } +} diff --git a/src/main/java/net/sourceforge/opencamera/UI/FolderChooserDialog.java b/src/main/java/net/sourceforge/opencamera/UI/FolderChooserDialog.java new file mode 100644 index 00000000..2ed78025 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/UI/FolderChooserDialog.java @@ -0,0 +1,354 @@ +package net.sourceforge.opencamera.UI; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.PreferenceKeys; +import net.sourceforge.opencamera.R; +import net.sourceforge.opencamera.StorageUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.text.InputFilter; +import android.text.Spanned; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +/** Dialog to pick a folder. Also allows creating new folders. Used when not + * using the Storage Access Framework. + */ +public class FolderChooserDialog extends DialogFragment { + private static final String TAG = "FolderChooserFragment"; + + private File current_folder; + private AlertDialog folder_dialog; + private ListView list; + private String chosen_folder; + + private static class FileWrapper implements Comparable { + private final File file; + private final String override_name; // if non-null, use this as the display name instead + private final int sort_order; // items are sorted first by sort_order, then alphabetically + + FileWrapper(File file, String override_name, int sort_order) { + this.file = file; + this.override_name = override_name; + this.sort_order = sort_order; + } + + @Override + public String toString() { + if( override_name != null ) + return override_name; + return file.getName(); + } + + @Override + public int compareTo(@NonNull FileWrapper o) { + if( this.sort_order < o.sort_order ) + return -1; + else if( this.sort_order > o.sort_order ) + return 1; + return this.file.getName().toLowerCase(Locale.US).compareTo(o.getFile().getName().toLowerCase(Locale.US)); + } + + @Override + public boolean equals(Object o) { + // important to override equals(), since we're overriding compareTo() + if( !(o instanceof FileWrapper) ) + return false; + FileWrapper that = (FileWrapper)o; + if( this.sort_order != that.sort_order ) + return false; + return this.file.getName().toLowerCase(Locale.US).equals(that.getFile().getName().toLowerCase(Locale.US)); + } + + @Override + public int hashCode() { + // must override this, as we override equals() + return this.file.getName().toLowerCase(Locale.US).hashCode(); + } + + File getFile() { + return file; + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if( MyDebug.LOG ) + Log.d(TAG, "onCreateDialog"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); + String folder_name = sharedPreferences.getString(PreferenceKeys.getSaveLocationPreferenceKey(), "OpenCamera"); + if( MyDebug.LOG ) + Log.d(TAG, "folder_name: " + folder_name); + File new_folder = StorageUtils.getImageFolder(folder_name); + if( MyDebug.LOG ) + Log.d(TAG, "start in folder: " + new_folder); + + list = new ListView(getActivity()); + list.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if( MyDebug.LOG ) + Log.d(TAG, "onItemClick: " + position); + FileWrapper file_wrapper = (FileWrapper) parent.getItemAtPosition(position); + if( MyDebug.LOG ) + Log.d(TAG, "clicked: " + file_wrapper.toString()); + File file = file_wrapper.getFile(); + if( MyDebug.LOG ) + Log.d(TAG, "file: " + file.toString()); + refreshList(file); + } + }); + folder_dialog = new AlertDialog.Builder(getActivity()) + //.setIcon(R.drawable.alert_dialog_icon) + .setView(list) + .setPositiveButton(R.string.use_folder, null) // we set the listener in onShowListener, so we can prevent the dialog from closing (if chosen folder isn't writable) + .setNeutralButton(R.string.new_folder, null) // we set the listener in onShowListener, so we can prevent the dialog from closing + .setNegativeButton(android.R.string.cancel, null) + .create(); + folder_dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog_interface) { + Button b_positive = folder_dialog.getButton(AlertDialog.BUTTON_POSITIVE); + b_positive.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "choose folder: " + current_folder.toString()); + if( useFolder() ) { + folder_dialog.dismiss(); + } + } + }); + Button b_neutral = folder_dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + b_neutral.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if( MyDebug.LOG ) + Log.d(TAG, "new folder in: " + current_folder.toString()); + newFolder(); + } + }); + } + }); + + if( !new_folder.exists() ) { + if( MyDebug.LOG ) + Log.d(TAG, "create new folder" + new_folder); + if( !new_folder.mkdirs() ) { + if( MyDebug.LOG ) + Log.d(TAG, "failed to create new folder"); + // don't do anything yet, this is handled below + } + } + refreshList(new_folder); + if( !canWrite() ) { + // see testFolderChooserInvalid() + if( MyDebug.LOG ) + Log.d(TAG, "failed to read folder"); + // note that we reset to DCIM rather than DCIM/OpenCamera, just to increase likelihood of getting back to a valid state + refreshList(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); + if( current_folder == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't even read DCIM?!"); + refreshList(new File("/")); + } + } + return folder_dialog; + } + + private void refreshList(File new_folder) { + if( MyDebug.LOG ) + Log.d(TAG, "refreshList: " + new_folder); + if( new_folder == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "refreshList: null folder"); + return; + } + File [] files = null; + // try/catch just in case? + try { + files = new_folder.listFiles(); + } + catch(Exception e) { + if( MyDebug.LOG ) + Log.d(TAG, "exception reading folder"); + e.printStackTrace(); + } + // n.b., files may be null if no files could be found in the folder (or we can't read) - but should still allow the user + // to view this folder (so the user can go to parent folders which might be readable again) + List listed_files = new ArrayList<>(); + if( new_folder.getParentFile() != null ) + listed_files.add(new FileWrapper(new_folder.getParentFile(), getResources().getString(R.string.parent_folder), 0)); + File default_folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + if( !default_folder.equals(new_folder) && !default_folder.equals(new_folder.getParentFile()) ) + listed_files.add(new FileWrapper(default_folder, null, 1)); + if( files != null ) { + for(File file : files) { + if( file.isDirectory() ) { + listed_files.add(new FileWrapper(file, null, 2)); + } + } + } + Collections.sort(listed_files); + + ArrayAdapter adapter = new ArrayAdapter<>(this.getActivity(), android.R.layout.simple_list_item_1, listed_files); + list.setAdapter(adapter); + + this.current_folder = new_folder; + //dialog.setTitle(current_folder.getName()); + folder_dialog.setTitle(current_folder.getAbsolutePath()); + } + + private boolean canWrite() { + try { + if( this.current_folder != null && this.current_folder.canWrite() ) + return true; + } + catch(Exception e) { + if( MyDebug.LOG ) + Log.d(TAG, "exception in canWrite()"); + } + return false; + } + + private boolean useFolder() { + if( MyDebug.LOG ) + Log.d(TAG, "useFolder"); + if( current_folder == null ) + return false; + if( canWrite() ) { + File base_folder = StorageUtils.getBaseFolder(); + String new_save_location = current_folder.getAbsolutePath(); + if( current_folder.getParentFile() != null && current_folder.getParentFile().equals(base_folder) ) { + if( MyDebug.LOG ) + Log.d(TAG, "parent folder is base folder"); + new_save_location = current_folder.getName(); + } + if( MyDebug.LOG ) + Log.d(TAG, "new_save_location: " + new_save_location); + chosen_folder = new_save_location; + return true; + } + else { + Toast.makeText(getActivity(), R.string.cant_write_folder, Toast.LENGTH_SHORT).show(); + } + return false; + } + + /** Returns the folder selected by the user. Returns null if the dialog was cancelled. + */ + public String getChosenFolder() { + return this.chosen_folder; + } + + private static class NewFolderInputFilter implements InputFilter { + // whilst Android seems to allow any characters on internal memory, SD cards are typically formatted with FAT32 + private final static String disallowed = "|\\?*<\":>"; + + @Override + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + for(int i=start;i= Build.VERSION_CODES.LOLLIPOP ) { + ColorStateList progress_color = ColorStateList.valueOf( Color.argb(255, 240, 240, 240) ); + ColorStateList thumb_color = ColorStateList.valueOf( Color.argb(255, 255, 255, 255) ); + + SeekBar seekBar = (SeekBar)main_activity.findViewById(R.id.zoom_seekbar); + seekBar.setProgressTintList(progress_color); + seekBar.setThumbTintList(thumb_color); + + seekBar = (SeekBar)main_activity.findViewById(R.id.focus_seekbar); + seekBar.setProgressTintList(progress_color); + seekBar.setThumbTintList(thumb_color); + + seekBar = (SeekBar)main_activity.findViewById(R.id.exposure_seekbar); + seekBar.setProgressTintList(progress_color); + seekBar.setThumbTintList(thumb_color); + + seekBar = (SeekBar)main_activity.findViewById(R.id.iso_seekbar); + seekBar.setProgressTintList(progress_color); + seekBar.setThumbTintList(thumb_color); + + seekBar = (SeekBar)main_activity.findViewById(R.id.exposure_time_seekbar); + seekBar.setProgressTintList(progress_color); + seekBar.setThumbTintList(thumb_color); + } + } + + /** Similar view.setRotation(ui_rotation), but achieves this via an animation. + */ + private void setViewRotation(View view, float ui_rotation) { + //view.setRotation(ui_rotation); + float rotate_by = ui_rotation - view.getRotation(); + if( rotate_by > 181.0f ) + rotate_by -= 360.0f; + else if( rotate_by < -181.0f ) + rotate_by += 360.0f; + // view.animate() modifies the view's rotation attribute, so it ends up equivalent to view.setRotation() + // we use rotationBy() instead of rotation(), so we get the minimal rotation for clockwise vs anti-clockwise + view.animate().rotationBy(rotate_by).setDuration(100).setInterpolator(new AccelerateDecelerateInterpolator()).start(); + } + + public void layoutUI() { + long debug_time = 0; + if( MyDebug.LOG ) { + Log.d(TAG, "layoutUI"); + debug_time = System.currentTimeMillis(); + } + //main_activity.getPreview().updateUIPlacement(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String ui_placement = sharedPreferences.getString(PreferenceKeys.getUIPlacementPreferenceKey(), "ui_right"); + // we cache the preference_ui_placement to save having to check it in the draw() method + this.ui_placement_right = ui_placement.equals("ui_right"); + if( MyDebug.LOG ) + Log.d(TAG, "ui_placement: " + ui_placement); + // new code for orientation fixed to landscape + // the display orientation should be locked to landscape, but how many degrees is that? + int rotation = main_activity.getWindowManager().getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + default: + break; + } + // getRotation is anti-clockwise, but current_orientation is clockwise, so we add rather than subtract + // relative_orientation is clockwise from landscape-left + //int relative_orientation = (current_orientation + 360 - degrees) % 360; + int relative_orientation = (current_orientation + degrees) % 360; + if( MyDebug.LOG ) { + Log.d(TAG, " current_orientation = " + current_orientation); + Log.d(TAG, " degrees = " + degrees); + Log.d(TAG, " relative_orientation = " + relative_orientation); + } + int ui_rotation = (360 - relative_orientation) % 360; + main_activity.getPreview().setUIRotation(ui_rotation); + int align_left = RelativeLayout.ALIGN_LEFT; + int align_right = RelativeLayout.ALIGN_RIGHT; + //int align_top = RelativeLayout.ALIGN_TOP; + //int align_bottom = RelativeLayout.ALIGN_BOTTOM; + int left_of = RelativeLayout.LEFT_OF; + int right_of = RelativeLayout.RIGHT_OF; + int above = RelativeLayout.ABOVE; + int below = RelativeLayout.BELOW; + int align_parent_left = RelativeLayout.ALIGN_PARENT_LEFT; + int align_parent_right = RelativeLayout.ALIGN_PARENT_RIGHT; + int align_parent_top = RelativeLayout.ALIGN_PARENT_TOP; + int align_parent_bottom = RelativeLayout.ALIGN_PARENT_BOTTOM; + if( !ui_placement_right ) { + //align_top = RelativeLayout.ALIGN_BOTTOM; + //align_bottom = RelativeLayout.ALIGN_TOP; + above = RelativeLayout.BELOW; + below = RelativeLayout.ABOVE; + align_parent_top = RelativeLayout.ALIGN_PARENT_BOTTOM; + align_parent_bottom = RelativeLayout.ALIGN_PARENT_TOP; + } + { + // we use a dummy button, so that the GUI buttons keep their positioning even if the Settings button is hidden (visibility set to View.GONE) + View view = main_activity.findViewById(R.id.gui_anchor); + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, 0); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.gallery); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.gui_anchor); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.settings); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.gallery); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.popup); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.settings); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.exposure_lock); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.popup); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.exposure); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.exposure_lock); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.switch_video); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.exposure); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.switch_camera); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.switch_video); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.audio_control); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.switch_camera); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.trash); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.audio_control); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.share); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(left_of, R.id.trash); + layoutParams.addRule(right_of, 0); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.take_photo); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.pause_video); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + view.setLayoutParams(layoutParams); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.zoom); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + view.setLayoutParams(layoutParams); + view.setRotation(180.0f); // should always match the zoom_seekbar, so that zoom in and out are in the same directions + + view = main_activity.findViewById(R.id.zoom_seekbar); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + // if we are showing the zoom control, the align next to that; otherwise have it aligned close to the edge of screen + if( sharedPreferences.getBoolean(PreferenceKeys.getShowZoomControlsPreferenceKey(), false) ) { + layoutParams.addRule(align_left, 0); + layoutParams.addRule(align_right, R.id.zoom); + layoutParams.addRule(above, R.id.zoom); + layoutParams.addRule(below, 0); + // need to clear the others, in case we turn zoom controls on/off + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + } + else { + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + // need to clear the others, in case we turn zoom controls on/off + layoutParams.addRule(align_left, 0); + layoutParams.addRule(align_right, 0); + layoutParams.addRule(above, 0); + layoutParams.addRule(below, 0); + } + view.setLayoutParams(layoutParams); + + view = main_activity.findViewById(R.id.focus_seekbar); + layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(align_left, R.id.preview); + layoutParams.addRule(align_right, 0); + layoutParams.addRule(left_of, R.id.zoom_seekbar); + layoutParams.addRule(right_of, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + view.setLayoutParams(layoutParams); + } + + { + // set seekbar info + int width_dp; + if( ui_rotation == 0 || ui_rotation == 180 ) { + width_dp = 300; + } + else { + width_dp = 200; + } + int height_dp = 50; + final float scale = main_activity.getResources().getDisplayMetrics().density; + int width_pixels = (int) (width_dp * scale + 0.5f); // convert dps to pixels + int height_pixels = (int) (height_dp * scale + 0.5f); // convert dps to pixels + int exposure_zoom_gap = (int) (4 * scale + 0.5f); // convert dps to pixels + + View view = main_activity.findViewById(R.id.exposure_container); + setViewRotation(view, ui_rotation); + view = main_activity.findViewById(R.id.exposure_seekbar); + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); + lp.width = width_pixels; + lp.height = height_pixels; + view.setLayoutParams(lp); + + view = main_activity.findViewById(R.id.exposure_seekbar_zoom); + setViewRotation(view, ui_rotation); + view.setAlpha(0.5f); + + // n.b., using left_of etc doesn't work properly when using rotation (as the amount of space reserved is based on the UI elements before being rotated) + if( ui_rotation == 0 ) { + view.setTranslationX(0); + view.setTranslationY(height_pixels+exposure_zoom_gap); + } + else if( ui_rotation == 90 ) { + view.setTranslationX(-height_pixels-exposure_zoom_gap); + view.setTranslationY(0); + } + else if( ui_rotation == 180 ) { + view.setTranslationX(0); + view.setTranslationY(-height_pixels-exposure_zoom_gap); + } + else if( ui_rotation == 270 ) { + view.setTranslationX(height_pixels+exposure_zoom_gap); + view.setTranslationY(0); + } + + view = main_activity.findViewById(R.id.manual_exposure_container); + setViewRotation(view, ui_rotation); + + view = main_activity.findViewById(R.id.iso_seekbar); + lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); + lp.width = width_pixels; + lp.height = height_pixels; + view.setLayoutParams(lp); + + view = main_activity.findViewById(R.id.exposure_time_seekbar); + lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); + lp.width = width_pixels; + lp.height = height_pixels; + view.setLayoutParams(lp); + } + + { + View view = main_activity.findViewById(R.id.popup_container); + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + //layoutParams.addRule(left_of, R.id.popup); + layoutParams.addRule(align_right, R.id.popup); + layoutParams.addRule(below, R.id.popup); + layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.addRule(above, 0); + layoutParams.addRule(align_parent_top, 0); + view.setLayoutParams(layoutParams); + + setViewRotation(view, ui_rotation); + // reset: + view.setTranslationX(0.0f); + view.setTranslationY(0.0f); + if( MyDebug.LOG ) { + Log.d(TAG, "popup view width: " + view.getWidth()); + Log.d(TAG, "popup view height: " + view.getHeight()); + } + if( ui_rotation == 0 || ui_rotation == 180 ) { + view.setPivotX(view.getWidth()/2.0f); + view.setPivotY(view.getHeight()/2.0f); + } + else { + view.setPivotX(view.getWidth()); + view.setPivotY(ui_placement_right ? 0.0f : view.getHeight()); + if( ui_placement_right ) { + if( ui_rotation == 90 ) + view.setTranslationY( view.getWidth() ); + else if( ui_rotation == 270 ) + view.setTranslationX( - view.getHeight() ); + } + else { + if( ui_rotation == 90 ) + view.setTranslationX( - view.getHeight() ); + else if( ui_rotation == 270 ) + view.setTranslationY( - view.getWidth() ); + } + } + } + + setTakePhotoIcon(); + // no need to call setSwitchCameraContentDescription() + + if( MyDebug.LOG ) { + Log.d(TAG, "layoutUI: total time: " + (System.currentTimeMillis() - debug_time)); + } + } + + /** Set icon for taking photos vs videos. + * Also handles content descriptions for the take photo button and switch video button. + */ + public void setTakePhotoIcon() { + if( MyDebug.LOG ) + Log.d(TAG, "setTakePhotoIcon()"); + if( main_activity.getPreview() != null ) { + ImageButton view = (ImageButton)main_activity.findViewById(R.id.take_photo); + int resource; + int content_description; + int switch_video_content_description; + if( main_activity.getPreview().isVideo() ) { + if( MyDebug.LOG ) + Log.d(TAG, "set icon to video"); + resource = main_activity.getPreview().isVideoRecording() ? R.drawable.take_video_recording : R.drawable.take_video_selector; + content_description = main_activity.getPreview().isVideoRecording() ? R.string.stop_video : R.string.start_video; + switch_video_content_description = R.string.switch_to_photo; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "set icon to photo"); + resource = R.drawable.take_photo_selector; + content_description = R.string.take_photo; + switch_video_content_description = R.string.switch_to_video; + } + view.setImageResource(resource); + view.setContentDescription( main_activity.getResources().getString(content_description) ); + view.setTag(resource); // for testing + + view = (ImageButton)main_activity.findViewById(R.id.switch_video); + view.setContentDescription( main_activity.getResources().getString(switch_video_content_description) ); + } + } + + /** Set content description for switch camera button. + */ + public void setSwitchCameraContentDescription() { + if( MyDebug.LOG ) + Log.d(TAG, "setSwitchCameraContentDescription()"); + if( main_activity.getPreview() != null && main_activity.getPreview().canSwitchCamera() ) { + ImageButton view = (ImageButton)main_activity.findViewById(R.id.switch_camera); + int content_description; + int cameraId = main_activity.getNextCameraId(); + if( main_activity.getPreview().getCameraControllerManager().isFrontFacing( cameraId ) ) { + content_description = R.string.switch_to_front_camera; + } + else { + content_description = R.string.switch_to_back_camera; + } + if( MyDebug.LOG ) + Log.d(TAG, "content_description: " + main_activity.getResources().getString(content_description)); + view.setContentDescription( main_activity.getResources().getString(content_description) ); + } + } + + /** Set content description for pause video button. + */ + public void setPauseVideoContentDescription() { + if (MyDebug.LOG) + Log.d(TAG, "setPauseVideoContentDescription()"); + View pauseVideoButton = main_activity.findViewById(R.id.pause_video); + int content_description; + if( main_activity.getPreview().isVideoRecordingPaused() ) { + content_description = R.string.resume_video; + } + else { + content_description = R.string.pause_video; + } + if( MyDebug.LOG ) + Log.d(TAG, "content_description: " + main_activity.getResources().getString(content_description)); + pauseVideoButton.setContentDescription(main_activity.getResources().getString(content_description)); + } + + public boolean getUIPlacementRight() { + return this.ui_placement_right; + } + + public void onOrientationChanged(int orientation) { + /*if( MyDebug.LOG ) { + Log.d(TAG, "onOrientationChanged()"); + Log.d(TAG, "orientation: " + orientation); + Log.d(TAG, "current_orientation: " + current_orientation); + }*/ + if( orientation == OrientationEventListener.ORIENTATION_UNKNOWN ) + return; + int diff = Math.abs(orientation - current_orientation); + if( diff > 180 ) + diff = 360 - diff; + // only change orientation when sufficiently changed + if( diff > 60 ) { + orientation = (orientation + 45) / 90 * 90; + orientation = orientation % 360; + if( orientation != current_orientation ) { + this.current_orientation = orientation; + if( MyDebug.LOG ) { + Log.d(TAG, "current_orientation is now: " + current_orientation); + } + layoutUI(); + } + } + } + + public void setImmersiveMode(final boolean immersive_mode) { + if( MyDebug.LOG ) + Log.d(TAG, "setImmersiveMode: " + immersive_mode); + this.immersive_mode = immersive_mode; + main_activity.runOnUiThread(new Runnable() { + public void run() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + // if going into immersive mode, the we should set GONE the ones that are set GONE in showGUI(false) + //final int visibility_gone = immersive_mode ? View.GONE : View.VISIBLE; + final int visibility = immersive_mode ? View.GONE : View.VISIBLE; + if( MyDebug.LOG ) + Log.d(TAG, "setImmersiveMode: set visibility: " + visibility); + // n.b., don't hide share and trash buttons, as they require immediate user input for us to continue + View switchCameraButton = main_activity.findViewById(R.id.switch_camera); + View switchVideoButton = main_activity.findViewById(R.id.switch_video); + View exposureButton = main_activity.findViewById(R.id.exposure); + View exposureLockButton = main_activity.findViewById(R.id.exposure_lock); + View audioControlButton = main_activity.findViewById(R.id.audio_control); + View popupButton = main_activity.findViewById(R.id.popup); + View galleryButton = main_activity.findViewById(R.id.gallery); + View settingsButton = main_activity.findViewById(R.id.settings); + View zoomControls = main_activity.findViewById(R.id.zoom); + View zoomSeekBar = main_activity.findViewById(R.id.zoom_seekbar); + if( main_activity.getPreview().getCameraControllerManager().getNumberOfCameras() > 1 ) + switchCameraButton.setVisibility(visibility); + switchVideoButton.setVisibility(visibility); + if( main_activity.supportsExposureButton() ) + exposureButton.setVisibility(visibility); + if( main_activity.getPreview().supportsExposureLock() ) + exposureLockButton.setVisibility(visibility); + if( main_activity.hasAudioControl() ) + audioControlButton.setVisibility(visibility); + popupButton.setVisibility(visibility); + galleryButton.setVisibility(visibility); + settingsButton.setVisibility(visibility); + if( MyDebug.LOG ) { + Log.d(TAG, "has_zoom: " + main_activity.getPreview().supportsZoom()); + } + if( main_activity.getPreview().supportsZoom() && sharedPreferences.getBoolean(PreferenceKeys.getShowZoomControlsPreferenceKey(), false) ) { + zoomControls.setVisibility(visibility); + } + if( main_activity.getPreview().supportsZoom() && sharedPreferences.getBoolean(PreferenceKeys.getShowZoomSliderControlsPreferenceKey(), true) ) { + zoomSeekBar.setVisibility(visibility); + } + String pref_immersive_mode = sharedPreferences.getString(PreferenceKeys.getImmersiveModePreferenceKey(), "immersive_mode_low_profile"); + if( pref_immersive_mode.equals("immersive_mode_everything") ) { + if( sharedPreferences.getBoolean(PreferenceKeys.getShowTakePhotoPreferenceKey(), true) ) { + View takePhotoButton = main_activity.findViewById(R.id.take_photo); + takePhotoButton.setVisibility(visibility); + } + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && main_activity.getPreview().isVideoRecording() ) { + View pauseVideoButton = main_activity.findViewById(R.id.pause_video); + pauseVideoButton.setVisibility(visibility); + } + } + if( !immersive_mode ) { + // make sure the GUI is set up as expected + showGUI(show_gui); + } + } + }); + } + + public boolean inImmersiveMode() { + return immersive_mode; + } + + public void showGUI(final boolean show) { + if( MyDebug.LOG ) + Log.d(TAG, "showGUI: " + show); + this.show_gui = show; + if( inImmersiveMode() ) + return; + if( show && main_activity.usingKitKatImmersiveMode() ) { + // call to reset the timer + main_activity.initImmersiveMode(); + } + main_activity.runOnUiThread(new Runnable() { + public void run() { + final int visibility = show ? View.VISIBLE : View.GONE; + View switchCameraButton = main_activity.findViewById(R.id.switch_camera); + View switchVideoButton = main_activity.findViewById(R.id.switch_video); + View exposureButton = main_activity.findViewById(R.id.exposure); + View exposureLockButton = main_activity.findViewById(R.id.exposure_lock); + View audioControlButton = main_activity.findViewById(R.id.audio_control); + View popupButton = main_activity.findViewById(R.id.popup); + if( main_activity.getPreview().getCameraControllerManager().getNumberOfCameras() > 1 ) + switchCameraButton.setVisibility(visibility); + if( !main_activity.getPreview().isVideo() ) + switchVideoButton.setVisibility(visibility); // still allow switch video when recording video + if( main_activity.supportsExposureButton() && !main_activity.getPreview().isVideo() ) // still allow exposure when recording video + exposureButton.setVisibility(visibility); + if( main_activity.getPreview().supportsExposureLock() && !main_activity.getPreview().isVideo() ) // still allow exposure lock when recording video + exposureLockButton.setVisibility(visibility); + if( main_activity.hasAudioControl() ) + audioControlButton.setVisibility(visibility); + if( !show ) { + closePopup(); // we still allow the popup when recording video, but need to update the UI (so it only shows flash options), so easiest to just close + } + if( !main_activity.getPreview().isVideo() || !main_activity.getPreview().supportsFlash() ) + popupButton.setVisibility(visibility); // still allow popup in order to change flash mode when recording video + } + }); + } + + public void audioControlStarted() { + ImageButton view = (ImageButton)main_activity.findViewById(R.id.audio_control); + view.setImageResource(R.drawable.ic_mic_red_48dp); + view.setContentDescription( main_activity.getResources().getString(R.string.audio_control_stop) ); + } + + public void audioControlStopped() { + ImageButton view = (ImageButton)main_activity.findViewById(R.id.audio_control); + view.setImageResource(R.drawable.ic_mic_white_48dp); + view.setContentDescription( main_activity.getResources().getString(R.string.audio_control_start) ); + } + + public void toggleExposureUI() { + if( MyDebug.LOG ) + Log.d(TAG, "toggleExposureUI"); + closePopup(); + View exposure_seek_bar = main_activity.findViewById(R.id.exposure_container); + int exposure_visibility = exposure_seek_bar.getVisibility(); + View manual_exposure_seek_bar = main_activity.findViewById(R.id.manual_exposure_container); + int manual_exposure_visibility = manual_exposure_seek_bar.getVisibility(); + boolean is_open = exposure_visibility == View.VISIBLE || manual_exposure_visibility == View.VISIBLE; + if( is_open ) { + clearSeekBar(); + } + else if( main_activity.getPreview().getCameraController() != null ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String value = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), main_activity.getPreview().getCameraController().getDefaultISO()); + if( main_activity.getPreview().usingCamera2API() && !value.equals("auto") ) { + // with Camera2 API, when using manual ISO we instead show sliders for ISO range and exposure time + if( main_activity.getPreview().supportsISORange()) { + manual_exposure_seek_bar.setVisibility(View.VISIBLE); + SeekBar exposure_time_seek_bar = ((SeekBar)main_activity.findViewById(R.id.exposure_time_seekbar)); + if( main_activity.getPreview().supportsExposureTime() ) { + exposure_time_seek_bar.setVisibility(View.VISIBLE); + } + else { + exposure_time_seek_bar.setVisibility(View.GONE); + } + } + } + else { + if( main_activity.getPreview().supportsExposures() ) { + exposure_seek_bar.setVisibility(View.VISIBLE); + ZoomControls seek_bar_zoom = (ZoomControls)main_activity.findViewById(R.id.exposure_seekbar_zoom); + seek_bar_zoom.setVisibility(View.VISIBLE); + } + } + } + } + + public void setSeekbarZoom() { + if( MyDebug.LOG ) + Log.d(TAG, "setSeekbarZoom"); + SeekBar zoomSeekBar = (SeekBar) main_activity.findViewById(R.id.zoom_seekbar); + zoomSeekBar.setProgress(main_activity.getPreview().getMaxZoom()-main_activity.getPreview().getCameraController().getZoom()); + if( MyDebug.LOG ) + Log.d(TAG, "progress is now: " + zoomSeekBar.getProgress()); + } + + public void changeSeekbar(int seekBarId, int change) { + if( MyDebug.LOG ) + Log.d(TAG, "changeSeekbar: " + change); + SeekBar seekBar = (SeekBar)main_activity.findViewById(seekBarId); + int value = seekBar.getProgress(); + int new_value = value + change; + if( new_value < 0 ) + new_value = 0; + else if( new_value > seekBar.getMax() ) + new_value = seekBar.getMax(); + if( MyDebug.LOG ) { + Log.d(TAG, "value: " + value); + Log.d(TAG, "new_value: " + new_value); + Log.d(TAG, "max: " + seekBar.getMax()); + } + if( new_value != value ) { + seekBar.setProgress(new_value); + } + } + + public void clearSeekBar() { + View view = main_activity.findViewById(R.id.exposure_container); + view.setVisibility(View.GONE); + view = main_activity.findViewById(R.id.exposure_seekbar_zoom); + view.setVisibility(View.GONE); + view = main_activity.findViewById(R.id.manual_exposure_container); + view.setVisibility(View.GONE); + } + + public void setPopupIcon() { + if( MyDebug.LOG ) + Log.d(TAG, "setPopupIcon"); + ImageButton popup = (ImageButton)main_activity.findViewById(R.id.popup); + String flash_value = main_activity.getPreview().getCurrentFlashValue(); + if( MyDebug.LOG ) + Log.d(TAG, "flash_value: " + flash_value); + if( flash_value != null && flash_value.equals("flash_off") ) { + popup.setImageResource(R.drawable.popup_flash_off); + } + else if( flash_value != null && flash_value.equals("flash_torch") ) { + popup.setImageResource(R.drawable.popup_flash_torch); + } + else if( flash_value != null && ( flash_value.equals("flash_auto") || flash_value.equals("flash_frontscreen_auto") ) ) { + popup.setImageResource(R.drawable.popup_flash_auto); + } + else if( flash_value != null && ( flash_value.equals("flash_on") || flash_value.equals("flash_frontscreen_on") ) ) { + popup.setImageResource(R.drawable.popup_flash_on); + } + else if( flash_value != null && flash_value.equals("flash_red_eye") ) { + popup.setImageResource(R.drawable.popup_flash_red_eye); + } + else { + popup.setImageResource(R.drawable.popup); + } + } + + public void closePopup() { + if( MyDebug.LOG ) + Log.d(TAG, "close popup"); + if( popupIsOpen() ) { + ViewGroup popup_container = (ViewGroup)main_activity.findViewById(R.id.popup_container); + popup_container.removeAllViews(); + popup_view_is_open = false; + /* Not destroying the popup doesn't really gain any performance. + * Also there are still outstanding bugs to fix if we wanted to do this: + * - Not resetting the popup menu when switching between photo and video mode. See test testVideoPopup(). + * - When changing options like flash/focus, the new option isn't selected when reopening the popup menu. See test + * testPopup(). + * - Changing settings potentially means we have to recreate the popup, so the natural place to do this is in + * MainActivity.updateForSettings(), but doing so makes the popup close when checking photo or video resolutions! + * See test testSwitchResolution(). + */ + destroyPopup(); + main_activity.initImmersiveMode(); // to reset the timer when closing the popup + } + } + + public boolean popupIsOpen() { + return popup_view_is_open; + } + + public void destroyPopup() { + if( popupIsOpen() ) { + closePopup(); + } + popup_view = null; + } + + public void togglePopupSettings() { + final ViewGroup popup_container = (ViewGroup)main_activity.findViewById(R.id.popup_container); + if( popupIsOpen() ) { + closePopup(); + return; + } + if( main_activity.getPreview().getCameraController() == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "camera not opened!"); + return; + } + + if( MyDebug.LOG ) + Log.d(TAG, "open popup"); + + clearSeekBar(); + main_activity.getPreview().cancelTimer(); // best to cancel any timer, in case we take a photo while settings window is open, or when changing settings + main_activity.stopAudioListeners(); + + final long time_s = System.currentTimeMillis(); + + { + // prevent popup being transparent + popup_container.setBackgroundColor(Color.BLACK); + popup_container.setAlpha(0.9f); + } + + if( popup_view == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "create new popup_view"); + popup_view = new PopupView(main_activity); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "use cached popup_view"); + } + popup_container.addView(popup_view); + popup_view_is_open = true; + + // need to call layoutUI to make sure the new popup is oriented correctly + // but need to do after the layout has been done, so we have a valid width/height to use + // n.b., even though we only need the portion of layoutUI for the popup container, there + // doesn't seem to be any performance benefit in only calling that part + popup_container.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onGlobalLayout() { + if( MyDebug.LOG ) + Log.d(TAG, "onGlobalLayout()"); + if( MyDebug.LOG ) + Log.d(TAG, "time after global layout: " + (System.currentTimeMillis() - time_s)); + layoutUI(); + if( MyDebug.LOG ) + Log.d(TAG, "time after layoutUI: " + (System.currentTimeMillis() - time_s)); + // stop listening - only want to call this once! + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + popup_container.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + popup_container.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String ui_placement = sharedPreferences.getString(PreferenceKeys.getUIPlacementPreferenceKey(), "ui_right"); + boolean ui_placement_right = ui_placement.equals("ui_right"); + ScaleAnimation animation = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 1.0f, Animation.RELATIVE_TO_SELF, ui_placement_right ? 0.0f : 1.0f); + animation.setDuration(100); + popup_container.setAnimation(animation); + } + } + ); + + if( MyDebug.LOG ) + Log.d(TAG, "time to create popup: " + (System.currentTimeMillis() - time_s)); + } + + @SuppressWarnings("deprecation") + public boolean onKeyDown(int keyCode, KeyEvent event) { + if( MyDebug.LOG ) + Log.d(TAG, "onKeyDown: " + keyCode); + switch( keyCode ) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: // media codes are for "selfie sticks" buttons + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_STOP: + { + if( keyCode == KeyEvent.KEYCODE_VOLUME_UP ) + keydown_volume_up = true; + else if( keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ) + keydown_volume_down = true; + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String volume_keys = sharedPreferences.getString(PreferenceKeys.getVolumeKeysPreferenceKey(), "volume_take_photo"); + + if((keyCode==KeyEvent.KEYCODE_MEDIA_PREVIOUS + ||keyCode==KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + ||keyCode==KeyEvent.KEYCODE_MEDIA_STOP) + &&!(volume_keys.equals("volume_take_photo"))) { + AudioManager audioManager = (AudioManager) main_activity.getSystemService(Context.AUDIO_SERVICE); + if(audioManager==null) break; + if(!audioManager.isWiredHeadsetOn()) break; // isWiredHeadsetOn() is deprecated, but comment says "Use only to check is a headset is connected or not." + } + + if( volume_keys.equals("volume_take_photo") ) { + main_activity.takePicture(); + return true; + } + else if( volume_keys.equals("volume_focus") ) { + if( keydown_volume_up && keydown_volume_down ) { + if( MyDebug.LOG ) + Log.d(TAG, "take photo rather than focus, as both volume keys are down"); + main_activity.takePicture(); + } + else if( main_activity.getPreview().getCurrentFocusValue() != null && main_activity.getPreview().getCurrentFocusValue().equals("focus_mode_manual2") ) { + if( keyCode == KeyEvent.KEYCODE_VOLUME_UP ) + main_activity.changeFocusDistance(-1); + else + main_activity.changeFocusDistance(1); + } + else { + // important not to repeatedly request focus, even though main_activity.getPreview().requestAutoFocus() will cancel, as causes problem if key is held down (e.g., flash gets stuck on) + // also check DownTime vs EventTime to prevent repeated focusing whilst the key is held down + if( event.getDownTime() == event.getEventTime() && !main_activity.getPreview().isFocusWaiting() ) { + if( MyDebug.LOG ) + Log.d(TAG, "request focus due to volume key"); + main_activity.getPreview().requestAutoFocus(); + } + } + return true; + } + else if( volume_keys.equals("volume_zoom") ) { + if( keyCode == KeyEvent.KEYCODE_VOLUME_UP ) + main_activity.zoomIn(); + else + main_activity.zoomOut(); + return true; + } + else if( volume_keys.equals("volume_exposure") ) { + if( main_activity.getPreview().getCameraController() != null ) { + String value = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), main_activity.getPreview().getCameraController().getDefaultISO()); + boolean manual_iso = !value.equals("auto"); + if( keyCode == KeyEvent.KEYCODE_VOLUME_UP ) { + if( manual_iso ) { + if( main_activity.getPreview().supportsISORange() ) + main_activity.changeISO(1); + } + else + main_activity.changeExposure(1); + } + else { + if( manual_iso ) { + if( main_activity.getPreview().supportsISORange() ) + main_activity.changeISO(-1); + } + else + main_activity.changeExposure(-1); + } + } + return true; + } + else if( volume_keys.equals("volume_auto_stabilise") ) { + if( main_activity.supportsAutoStabilise() ) { + boolean auto_stabilise = sharedPreferences.getBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), false); + auto_stabilise = !auto_stabilise; + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PreferenceKeys.getAutoStabilisePreferenceKey(), auto_stabilise); + editor.apply(); + String message = main_activity.getResources().getString(R.string.preference_auto_stabilise) + ": " + main_activity.getResources().getString(auto_stabilise ? R.string.on : R.string.off); + main_activity.getPreview().showToast(main_activity.getChangedAutoStabiliseToastBoxer(), message); + } + else { + main_activity.getPreview().showToast(main_activity.getChangedAutoStabiliseToastBoxer(), R.string.auto_stabilise_not_supported); + } + return true; + } + else if( volume_keys.equals("volume_really_nothing") ) { + // do nothing, but still return true so we don't change volume either + return true; + } + // else do nothing here, but still allow changing of volume (i.e., the default behaviour) + break; + } + case KeyEvent.KEYCODE_MENU: + { + // needed to support hardware menu button + // tested successfully on Samsung S3 (via RTL) + // see http://stackoverflow.com/questions/8264611/how-to-detect-when-user-presses-menu-key-on-their-android-device + main_activity.openSettings(); + return true; + } + case KeyEvent.KEYCODE_CAMERA: + { + if( event.getRepeatCount() == 0 ) { + main_activity.takePicture(); + return true; + } + } + case KeyEvent.KEYCODE_FOCUS: + { + // important not to repeatedly request focus, even though main_activity.getPreview().requestAutoFocus() will cancel - causes problem with hardware camera key where a half-press means to focus + // also check DownTime vs EventTime to prevent repeated focusing whilst the key is held down - see https://sourceforge.net/p/opencamera/tickets/174/ , + // or same issue above for volume key focus + if( event.getDownTime() == event.getEventTime() && !main_activity.getPreview().isFocusWaiting() ) { + if( MyDebug.LOG ) + Log.d(TAG, "request focus due to focus key"); + main_activity.getPreview().requestAutoFocus(); + } + return true; + } + case KeyEvent.KEYCODE_ZOOM_IN: + { + main_activity.zoomIn(); + return true; + } + case KeyEvent.KEYCODE_ZOOM_OUT: + { + main_activity.zoomOut(); + return true; + } + } + return false; + } + + public void onKeyUp(int keyCode, KeyEvent event) { + if( MyDebug.LOG ) + Log.d(TAG, "onKeyUp: " + keyCode); + if( keyCode == KeyEvent.KEYCODE_VOLUME_UP ) + keydown_volume_up = false; + else if( keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ) + keydown_volume_down = false; + } + + // for testing + public View getPopupButton(String key) { + return popup_view.getPopupButton(key); + } +} diff --git a/src/main/java/net/sourceforge/opencamera/UI/PopupView.java b/src/main/java/net/sourceforge/opencamera/UI/PopupView.java new file mode 100644 index 00000000..7edd1612 --- /dev/null +++ b/src/main/java/net/sourceforge/opencamera/UI/PopupView.java @@ -0,0 +1,957 @@ +package net.sourceforge.opencamera.UI; + +import net.sourceforge.opencamera.MainActivity; +import net.sourceforge.opencamera.MyApplicationInterface; +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.PreferenceKeys; +import net.sourceforge.opencamera.R; +import net.sourceforge.opencamera.CameraController.CameraController; +import net.sourceforge.opencamera.Preview.Preview; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.HorizontalScrollView; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.ImageView.ScaleType; + +/** This defines the UI for the "popup" button, that provides quick access to a + * range of options. + */ +public class PopupView extends LinearLayout { + private static final String TAG = "PopupView"; + public static final float ALPHA_BUTTON_SELECTED = 1.0f; + public static final float ALPHA_BUTTON = 0.6f; + + private int picture_size_index = -1; + private int video_size_index = -1; + private int timer_index = -1; + private int burst_mode_index = -1; + private int grid_index = -1; + + private final Map popup_buttons = new Hashtable<>(); + + public PopupView(Context context) { + super(context); + if( MyDebug.LOG ) + Log.d(TAG, "new PopupView: " + this); + + this.setOrientation(LinearLayout.VERTICAL); + + final MainActivity main_activity = (MainActivity)this.getContext(); + final Preview preview = main_activity.getPreview(); + { + List supported_flash_values = preview.getSupportedFlashValues(); + addButtonOptionsToPopup(supported_flash_values, R.array.flash_icons, R.array.flash_values, getResources().getString(R.string.flash_mode), preview.getCurrentFlashValue(), "TEST_FLASH", new ButtonOptionsPopupListener() { + @Override + public void onClick(String option) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked flash: " + option); + preview.updateFlash(option); + main_activity.getMainUI().setPopupIcon(); + main_activity.closePopup(); + } + }); + } + + if( preview.isVideo() && preview.isTakingPhoto() ) { + // don't add any more options + } + else { + // make a copy of getSupportedFocusValues() so we can modify it + List supported_focus_values = preview.getSupportedFocusValues(); + if( supported_focus_values != null ) { + supported_focus_values = new ArrayList<>(supported_focus_values); + // only show appropriate continuous focus mode + if( preview.isVideo() ) { + supported_focus_values.remove("focus_mode_continuous_picture"); + } + else { + supported_focus_values.remove("focus_mode_continuous_video"); + } + } + addButtonOptionsToPopup(supported_focus_values, R.array.focus_mode_icons, R.array.focus_mode_values, getResources().getString(R.string.focus_mode), preview.getCurrentFocusValue(), "TEST_FOCUS", new ButtonOptionsPopupListener() { + @Override + public void onClick(String option) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked focus: " + option); + preview.updateFocus(option, false, true); + main_activity.closePopup(); + } + }); + + List supported_isos; + final String manual_value = "m"; + if( preview.supportsISORange() ) { + if( MyDebug.LOG ) + Log.d(TAG, "supports ISO range"); + int min_iso = preview.getMinimumISO(); + int max_iso = preview.getMaximumISO(); + List values = new ArrayList<>(); + values.add("auto"); + values.add(manual_value); + int [] iso_values = {50, 100, 200, 400, 800, 1600, 3200, 6400}; + values.add("" + min_iso); + for(int iso_value : iso_values) { + if( iso_value > min_iso && iso_value < max_iso ) { + values.add("" + iso_value); + } + } + values.add("" + max_iso); + supported_isos = values; + } + else { + supported_isos = preview.getSupportedISOs(); + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String current_iso = sharedPreferences.getString(PreferenceKeys.getISOPreferenceKey(), "auto"); + // if the manual ISO value isn't one of the "preset" values, then instead highlight the manual ISO icon + if( !current_iso.equals("auto") && supported_isos != null && supported_isos.contains(manual_value) && !supported_isos.contains(current_iso) ) + current_iso = manual_value; + // n.b., we hardcode the string "ISO" as we don't want it translated - firstly more consistent with the ISO values returned by the driver, secondly need to worry about the size of the buttons, so don't want risk of a translated string being too long + addButtonOptionsToPopup(supported_isos, -1, -1, "ISO", current_iso, "TEST_ISO", new ButtonOptionsPopupListener() { + @Override + public void onClick(String option) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked iso: " + option); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getISOPreferenceKey(), option); + String toast_option = option; + if( option.equals("auto") ) { + if( MyDebug.LOG ) + Log.d(TAG, "switched from manual to auto iso"); + // also reset exposure time when changing from manual to auto from the popup menu: + editor.putLong(PreferenceKeys.getExposureTimePreferenceKey(), CameraController.EXPOSURE_TIME_DEFAULT); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "switched from auto to manual iso"); + if( option.equals("m") ) { + // if we used the generic "manual", then instead try to preserve the current iso if it exists + if( preview.getCameraController() != null && preview.getCameraController().captureResultHasIso() ) { + int iso = preview.getCameraController().captureResultIso(); + if( MyDebug.LOG ) + Log.d(TAG, "apply existing iso of " + iso); + editor.putString(PreferenceKeys.getISOPreferenceKey(), "" + iso); + toast_option = "" + iso; + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no existing iso available"); + // use a default + final int iso = 800; + editor.putString(PreferenceKeys.getISOPreferenceKey(), "" + iso); + toast_option = "" + iso; + } + } + if( preview.usingCamera2API() ) { + // if changing from auto to manual, preserve the current exposure time if it exists + if( preview.getCameraController() != null && preview.getCameraController().captureResultHasExposureTime() ) { + long exposure_time = preview.getCameraController().captureResultExposureTime(); + if( MyDebug.LOG ) + Log.d(TAG, "apply existing exposure time of " + exposure_time); + editor.putLong(PreferenceKeys.getExposureTimePreferenceKey(), exposure_time); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "no existing exposure time available"); + } + } + } + editor.apply(); + + main_activity.updateForSettings("ISO: " + toast_option); + main_activity.closePopup(); + } + }); + + final List photo_modes = new ArrayList<>(); + final List photo_mode_values = new ArrayList<>(); + photo_modes.add( getResources().getString(R.string.photo_mode_standard) ); + photo_mode_values.add( MyApplicationInterface.PhotoMode.Standard ); + if( main_activity.supportsDRO() ) { + photo_modes.add( getResources().getString(R.string.photo_mode_dro) ); + photo_mode_values.add( MyApplicationInterface.PhotoMode.DRO ); + } + if( main_activity.supportsHDR() ) { + photo_modes.add( getResources().getString(R.string.photo_mode_hdr) ); + photo_mode_values.add( MyApplicationInterface.PhotoMode.HDR ); + } + if( main_activity.supportsExpoBracketing() ) { + photo_modes.add( getResources().getString(R.string.photo_mode_expo_bracketing) ); + photo_mode_values.add( MyApplicationInterface.PhotoMode.ExpoBracketing ); + } + if( photo_modes.size() > 1 ) { + MyApplicationInterface.PhotoMode photo_mode = main_activity.getApplicationInterface().getPhotoMode(); + String current_mode = null; + for(int i=0;i picture_sizes = preview.getSupportedPictureSizes(); + picture_size_index = preview.getCurrentPictureSizeIndex(); + final List picture_size_strings = new ArrayList<>(); + for(CameraController.Size picture_size : picture_sizes) { + String size_string = picture_size.width + " x " + picture_size.height + " " + Preview.getMPString(picture_size.width, picture_size.height); + picture_size_strings.add(size_string); + } + addArrayOptionsToPopup(picture_size_strings, getResources().getString(R.string.preference_resolution), false, picture_size_index, false, "PHOTO_RESOLUTIONS", new ArrayOptionsPopupListener() { + final Handler handler = new Handler(); + final Runnable update_runnable = new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "update settings due to resolution change"); + main_activity.updateForSettings(""); + } + }; + + private void update() { + if( picture_size_index == -1 ) + return; + CameraController.Size new_size = picture_sizes.get(picture_size_index); + String resolution_string = new_size.width + " " + new_size.height; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getResolutionPreferenceKey(preview.getCameraId()), resolution_string); + editor.apply(); + + // make it easier to scroll through the list of resolutions without a pause each time + handler.removeCallbacks(update_runnable); + handler.postDelayed(update_runnable, 400); + } + @Override + public int onClickPrev() { + if( picture_size_index != -1 && picture_size_index > 0 ) { + picture_size_index--; + update(); + return picture_size_index; + } + return -1; + } + @Override + public int onClickNext() { + if( picture_size_index != -1 && picture_size_index < picture_sizes.size()-1 ) { + picture_size_index++; + update(); + return picture_size_index; + } + return -1; + } + }); + + final List video_sizes = preview.getVideoQualityHander().getSupportedVideoQuality(); + video_size_index = preview.getVideoQualityHander().getCurrentVideoQualityIndex(); + final List video_size_strings = new ArrayList<>(); + for(String video_size : video_sizes) { + String quality_string = preview.getCamcorderProfileDescriptionShort(video_size); + video_size_strings.add(quality_string); + } + addArrayOptionsToPopup(video_size_strings, getResources().getString(R.string.video_quality), false, video_size_index, false, "VIDEO_RESOLUTIONS", new ArrayOptionsPopupListener() { + final Handler handler = new Handler(); + final Runnable update_runnable = new Runnable() { + @Override + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "update settings due to video resolution change"); + main_activity.updateForSettings(""); + } + }; + + private void update() { + if( video_size_index == -1 ) + return; + String quality = video_sizes.get(video_size_index); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getVideoQualityPreferenceKey(preview.getCameraId()), quality); + editor.apply(); + + // make it easier to scroll through the list of resolutions without a pause each time + handler.removeCallbacks(update_runnable); + handler.postDelayed(update_runnable, 400); + } + @Override + public int onClickPrev() { + if( video_size_index != -1 && video_size_index > 0 ) { + video_size_index--; + update(); + return video_size_index; + } + return -1; + } + @Override + public int onClickNext() { + if( video_size_index != -1 && video_size_index < video_sizes.size()-1 ) { + video_size_index++; + update(); + return video_size_index; + } + return -1; + } + }); + + final String [] timer_values = getResources().getStringArray(R.array.preference_timer_values); + String [] timer_entries = getResources().getStringArray(R.array.preference_timer_entries); + String timer_value = sharedPreferences.getString(PreferenceKeys.getTimerPreferenceKey(), "0"); + timer_index = Arrays.asList(timer_values).indexOf(timer_value); + if( timer_index == -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't find timer_value " + timer_value + " in timer_values!"); + timer_index = 0; + } + addArrayOptionsToPopup(Arrays.asList(timer_entries), getResources().getString(R.string.preference_timer), true, timer_index, false, "TIMER", new ArrayOptionsPopupListener() { + private void update() { + if( timer_index == -1 ) + return; + String new_timer_value = timer_values[timer_index]; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getTimerPreferenceKey(), new_timer_value); + editor.apply(); + } + @Override + public int onClickPrev() { + if( timer_index != -1 && timer_index > 0 ) { + timer_index--; + update(); + return timer_index; + } + return -1; + } + @Override + public int onClickNext() { + if( timer_index != -1 && timer_index < timer_values.length-1 ) { + timer_index++; + update(); + return timer_index; + } + return -1; + } + }); + + final String [] burst_mode_values = getResources().getStringArray(R.array.preference_burst_mode_values); + String [] burst_mode_entries = getResources().getStringArray(R.array.preference_burst_mode_entries); + String burst_mode_value = sharedPreferences.getString(PreferenceKeys.getBurstModePreferenceKey(), "1"); + burst_mode_index = Arrays.asList(burst_mode_values).indexOf(burst_mode_value); + if( burst_mode_index == -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't find burst_mode_value " + burst_mode_value + " in burst_mode_values!"); + burst_mode_index = 0; + } + addArrayOptionsToPopup(Arrays.asList(burst_mode_entries), getResources().getString(R.string.preference_burst_mode), true, burst_mode_index, false, "BURST_MODE", new ArrayOptionsPopupListener() { + private void update() { + if( burst_mode_index == -1 ) + return; + String new_burst_mode_value = burst_mode_values[burst_mode_index]; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getBurstModePreferenceKey(), new_burst_mode_value); + editor.apply(); + } + @Override + public int onClickPrev() { + if( burst_mode_index != -1 && burst_mode_index > 0 ) { + burst_mode_index--; + update(); + return burst_mode_index; + } + return -1; + } + @Override + public int onClickNext() { + if( burst_mode_index != -1 && burst_mode_index < burst_mode_values.length-1 ) { + burst_mode_index++; + update(); + return burst_mode_index; + } + return -1; + } + }); + + final String [] grid_values = getResources().getStringArray(R.array.preference_grid_values); + String [] grid_entries = getResources().getStringArray(R.array.preference_grid_entries); + String grid_value = sharedPreferences.getString(PreferenceKeys.getShowGridPreferenceKey(), "preference_grid_none"); + grid_index = Arrays.asList(grid_values).indexOf(grid_value); + if( grid_index == -1 ) { + if( MyDebug.LOG ) + Log.d(TAG, "can't find grid_value " + grid_value + " in grid_values!"); + grid_index = 0; + } + addArrayOptionsToPopup(Arrays.asList(grid_entries), getResources().getString(R.string.grid), false, grid_index, true, "GRID", new ArrayOptionsPopupListener() { + private void update() { + if( grid_index == -1 ) + return; + String new_grid_value = grid_values[grid_index]; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.getShowGridPreferenceKey(), new_grid_value); + editor.apply(); + } + @Override + public int onClickPrev() { + if( grid_index != -1 ) { + grid_index--; + if( grid_index < 0 ) + grid_index += grid_values.length; + update(); + return grid_index; + } + return -1; + } + @Override + public int onClickNext() { + if( grid_index != -1 ) { + grid_index++; + if( grid_index >= grid_values.length ) + grid_index -= grid_values.length; + update(); + return grid_index; + } + return -1; + } + }); + + // popup should only be opened if we have a camera controller, but check just to be safe + if( preview.getCameraController() != null ) { + List supported_white_balances = preview.getSupportedWhiteBalances(); + addRadioOptionsToPopup(supported_white_balances, getResources().getString(R.string.white_balance), PreferenceKeys.getWhiteBalancePreferenceKey(), preview.getCameraController().getDefaultWhiteBalance(), "TEST_WHITE_BALANCE"); + + List supported_scene_modes = preview.getSupportedSceneModes(); + addRadioOptionsToPopup(supported_scene_modes, getResources().getString(R.string.scene_mode), PreferenceKeys.getSceneModePreferenceKey(), preview.getCameraController().getDefaultSceneMode(), "TEST_SCENE_MODE"); + + List supported_color_effects = preview.getSupportedColorEffects(); + addRadioOptionsToPopup(supported_color_effects, getResources().getString(R.string.color_effect), PreferenceKeys.getColorEffectPreferenceKey(), preview.getCameraController().getDefaultColorEffect(), "TEST_COLOR_EFFECT"); + } + + } + } + + private abstract class ButtonOptionsPopupListener { + public abstract void onClick(String option); + } + + private void addButtonOptionsToPopup(List supported_options, int icons_id, int values_id, String prefix_string, String current_value, String test_key, final ButtonOptionsPopupListener listener) { + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup"); + if( supported_options != null ) { + final long debug_time = System.currentTimeMillis(); + LinearLayout ll2 = new LinearLayout(this.getContext()); + ll2.setOrientation(LinearLayout.HORIZONTAL); + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 1: " + (System.currentTimeMillis() - debug_time)); + String [] icons = icons_id != -1 ? getResources().getStringArray(icons_id) : null; + String [] values = values_id != -1 ? getResources().getStringArray(values_id) : null; + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2: " + (System.currentTimeMillis() - debug_time)); + + final float scale = getResources().getDisplayMetrics().density; + int total_width = 280; + { + Activity activity = (Activity)this.getContext(); + Display display = activity.getWindowManager().getDefaultDisplay(); + DisplayMetrics outMetrics = new DisplayMetrics(); + display.getMetrics(outMetrics); + + // the height should limit the width, due to when held in portrait + int dpHeight = (int)(outMetrics.heightPixels / scale); + if( MyDebug.LOG ) + Log.d(TAG, "dpHeight: " + dpHeight); + dpHeight -= 50; // allow space for the icons at top/right of screen + if( total_width > dpHeight ) + total_width = dpHeight; + } + if( MyDebug.LOG ) + Log.d(TAG, "total_width: " + total_width); + int button_width_dp = total_width/supported_options.size(); + boolean use_scrollview = false; + if( button_width_dp < 40 ) { + button_width_dp = 40; + use_scrollview = true; + } + View current_view = null; + + for(final String supported_option : supported_options) { + if( MyDebug.LOG ) + Log.d(TAG, "supported_option: " + supported_option); + int resource = -1; + if( icons != null && values != null ) { + int index = -1; + for(int i=0;i= 4 && supported_option.substring(0, 4).equalsIgnoreCase("ISO_") ) { + button_string = prefix_string + "\n" + supported_option.substring(4); + } + else if( prefix_string.equalsIgnoreCase("ISO") && supported_option.length() >= 3 && supported_option.substring(0, 3).equalsIgnoreCase("ISO") ) { + button_string = prefix_string + "\n" + supported_option.substring(3); + } + else { + button_string = prefix_string + "\n" + supported_option; + } + if( MyDebug.LOG ) + Log.d(TAG, "button_string: " + button_string); + View view; + if( resource != -1 ) { + ImageButton image_button = new ImageButton(this.getContext()); + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2.11: " + (System.currentTimeMillis() - debug_time)); + view = image_button; + ll2.addView(view); + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2.12: " + (System.currentTimeMillis() - debug_time)); + + //image_button.setImageResource(resource); + final MainActivity main_activity = (MainActivity)this.getContext(); + Bitmap bm = main_activity.getPreloadedBitmap(resource); + if( bm != null ) + image_button.setImageBitmap(bm); + else { + if( MyDebug.LOG ) + Log.d(TAG, "failed to find bitmap for resource " + resource + "!"); + } + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2.13: " + (System.currentTimeMillis() - debug_time)); + image_button.setScaleType(ScaleType.FIT_CENTER); + final int padding = (int) (10 * scale + 0.5f); // convert dps to pixels + view.setPadding(padding, padding, padding, padding); + } + else { + Button button = new Button(this.getContext()); + button.setBackgroundColor(Color.TRANSPARENT); // workaround for Android 6 crash! + view = button; + ll2.addView(view); + + button.setText(button_string); + button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.0f); + button.setTextColor(Color.WHITE); + // need 0 padding so we have enough room to display text for ISO buttons, when there are 6 ISO settings + final int padding = (int) (0 * scale + 0.5f); // convert dps to pixels + view.setPadding(padding, padding, padding, padding); + } + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2.2: " + (System.currentTimeMillis() - debug_time)); + + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.width = (int) (button_width_dp * scale + 0.5f); // convert dps to pixels + params.height = (int) (50 * scale + 0.5f); // convert dps to pixels + view.setLayoutParams(params); + + view.setContentDescription(button_string); + if( supported_option.equals(current_value) ) { + view.setAlpha(ALPHA_BUTTON_SELECTED); + current_view = view; + } + else { + view.setAlpha(ALPHA_BUTTON); + } + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 2.3: " + (System.currentTimeMillis() - debug_time)); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked: " + supported_option); + listener.onClick(supported_option); + } + }); + this.popup_buttons.put(test_key + "_" + supported_option, view); + if( MyDebug.LOG ) { + Log.d(TAG, "addButtonOptionsToPopup time 2.4: " + (System.currentTimeMillis() - debug_time)); + Log.d(TAG, "added to popup_buttons: " + test_key + "_" + supported_option + " view: " + view); + Log.d(TAG, "popup_buttons is now: " + popup_buttons); + } + } + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 3: " + (System.currentTimeMillis() - debug_time)); + if( use_scrollview ) { + if( MyDebug.LOG ) + Log.d(TAG, "using scrollview"); + final HorizontalScrollView scroll = new HorizontalScrollView(this.getContext()); + scroll.addView(ll2); + { + ViewGroup.LayoutParams params = new LayoutParams( + (int) (total_width * scale + 0.5f), // convert dps to pixels + LayoutParams.WRAP_CONTENT); + scroll.setLayoutParams(params); + } + this.addView(scroll); + if( current_view != null ) { + // scroll to the selected button + final View final_current_view = current_view; + this.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + /*if( MyDebug.LOG ) + Log.d(TAG, "jump to " + final_current_view.getLeft());*/ + scroll.scrollTo(final_current_view.getLeft(), 0); + } + } + ); + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "not using scrollview"); + this.addView(ll2); + } + if( MyDebug.LOG ) + Log.d(TAG, "addButtonOptionsToPopup time 4: " + (System.currentTimeMillis() - debug_time)); + } + } + + private void addTitleToPopup(final String title) { + TextView text_view = new TextView(this.getContext()); + text_view.setText(title + ":"); + text_view.setTextColor(Color.WHITE); + text_view.setGravity(Gravity.CENTER); + text_view.setTypeface(null, Typeface.BOLD); + //text_view.setBackgroundColor(Color.GRAY); // debug + this.addView(text_view); + } + + private void addRadioOptionsToPopup(List supported_options, final String title, final String preference_key, final String default_option, final String test_key) { + if( MyDebug.LOG ) + Log.d(TAG, "addOptionsToPopup: " + title); + if( supported_options != null ) { + final MainActivity main_activity = (MainActivity)this.getContext(); + + addTitleToPopup(title); + + RadioGroup rg = new RadioGroup(this.getContext()); + rg.setOrientation(RadioGroup.VERTICAL); + this.popup_buttons.put(test_key, rg); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + String current_option = sharedPreferences.getString(preference_key, default_option); + for(final String supported_option : supported_options) { + if( MyDebug.LOG ) + Log.d(TAG, "supported_option: " + supported_option); + //Button button = new Button(this); + RadioButton button = new RadioButton(this.getContext()); + button.setText(supported_option); + button.setTextColor(Color.WHITE); + if( supported_option.equals(current_option) ) { + button.setChecked(true); + } + else { + button.setChecked(false); + } + //ll.addView(button); + rg.addView(button); + button.setContentDescription(supported_option); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if( MyDebug.LOG ) + Log.d(TAG, "clicked current_option: " + supported_option); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(preference_key, supported_option); + editor.apply(); + + main_activity.updateForSettings(title + ": " + supported_option); + main_activity.closePopup(); + } + }); + this.popup_buttons.put(test_key + "_" + supported_option, button); + } + this.addView(rg); + } + } + + private abstract class ArrayOptionsPopupListener { + public abstract int onClickPrev(); + public abstract int onClickNext(); + } + + private void addArrayOptionsToPopup(final List supported_options, final String title, final boolean title_in_options, final int current_index, final boolean cyclic, final String test_key, final ArrayOptionsPopupListener listener) { + if( supported_options != null && current_index != -1 ) { + if( !title_in_options ) { + addTitleToPopup(title); + } + + /*final Button prev_button = new Button(this.getContext()); + //prev_button.setBackgroundResource(R.drawable.exposure); + prev_button.setBackgroundColor(Color.TRANSPARENT); // workaround for Android 6 crash! + prev_button.setText("<"); + this.addView(prev_button);*/ + + LinearLayout ll2 = new LinearLayout(this.getContext()); + ll2.setOrientation(LinearLayout.HORIZONTAL); + + final TextView resolution_text_view = new TextView(this.getContext()); + if( title_in_options ) + resolution_text_view.setText(title + ": " + supported_options.get(current_index)); + else + resolution_text_view.setText(supported_options.get(current_index)); + resolution_text_view.setTextColor(Color.WHITE); + resolution_text_view.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f); + resolution_text_view.setLayoutParams(params); + + final float scale = getResources().getDisplayMetrics().density; + final int padding = (int) (0 * scale + 0.5f); // convert dps to pixels + final int button_w = (int) (60 * scale + 0.5f); // convert dps to pixels + final int button_h = (int) (30 * scale + 0.5f); // convert dps to pixels + final Button prev_button = new Button(this.getContext()); + prev_button.setBackgroundColor(Color.TRANSPARENT); // workaround for Android 6 crash! + ll2.addView(prev_button); + prev_button.setText("<"); + prev_button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.0f); + prev_button.setPadding(padding, padding, padding, padding); + ViewGroup.LayoutParams vg_params = prev_button.getLayoutParams(); + vg_params.width = button_w; + vg_params.height = button_h; + prev_button.setLayoutParams(vg_params); + prev_button.setVisibility( (cyclic || current_index > 0) ? View.VISIBLE : View.INVISIBLE); + this.popup_buttons.put(test_key + "_PREV", prev_button); + + ll2.addView(resolution_text_view); + this.popup_buttons.put(test_key, resolution_text_view); + + final Button next_button = new Button(this.getContext()); + next_button.setBackgroundColor(Color.TRANSPARENT); // workaround for Android 6 crash! + ll2.addView(next_button); + next_button.setText(">"); + next_button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.0f); + next_button.setPadding(padding, padding, padding, padding); + vg_params = next_button.getLayoutParams(); + vg_params.width = button_w; + vg_params.height = button_h; + next_button.setLayoutParams(vg_params); + next_button.setVisibility( (cyclic || current_index < supported_options.size()-1) ? View.VISIBLE : View.INVISIBLE); + this.popup_buttons.put(test_key + "_NEXT", next_button); + + prev_button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int new_index = listener.onClickPrev(); + if( new_index != -1 ) { + if( title_in_options ) + resolution_text_view.setText(title + ": " + supported_options.get(new_index)); + else + resolution_text_view.setText(supported_options.get(new_index)); + prev_button.setVisibility( (cyclic || new_index > 0) ? View.VISIBLE : View.INVISIBLE); + next_button.setVisibility( (cyclic || new_index < supported_options.size()-1) ? View.VISIBLE : View.INVISIBLE); + } + } + }); + next_button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int new_index = listener.onClickNext(); + if( new_index != -1 ) { + if( title_in_options ) + resolution_text_view.setText(title + ": " + supported_options.get(new_index)); + else + resolution_text_view.setText(supported_options.get(new_index)); + prev_button.setVisibility( (cyclic || new_index > 0) ? View.VISIBLE : View.INVISIBLE); + next_button.setVisibility( (cyclic || new_index < supported_options.size()-1) ? View.VISIBLE : View.INVISIBLE); + } + } + }); + + this.addView(ll2); + } + } + + private void showInfoDialog(int title_id, int info_id, final String info_preference_key) { + final MainActivity main_activity = (MainActivity)this.getContext(); + AlertDialog.Builder alertDialog = new AlertDialog.Builder(PopupView.this.getContext()); + alertDialog.setTitle(title_id); + alertDialog.setMessage(info_id); + alertDialog.setPositiveButton(android.R.string.ok, null); + alertDialog.setNegativeButton(R.string.dont_show_again, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if( MyDebug.LOG ) + Log.d(TAG, "user clicked dont_show_again for info dialog"); + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(info_preference_key, true); + editor.apply(); + } + }); + + main_activity.showPreview(false); + main_activity.setWindowFlagsForSettings(); + + AlertDialog alert = alertDialog.create(); + // AlertDialog.Builder.setOnDismissListener() requires API level 17, so do it this way instead + alert.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface arg0) { + if( MyDebug.LOG ) + Log.d(TAG, "info dialog dismissed"); + main_activity.setWindowFlagsForCamera(); + main_activity.showPreview(true); + } + }); + alert.show(); + } + + // for testing + public View getPopupButton(String key) { + if( MyDebug.LOG ) { + Log.d(TAG, "getPopupButton(" + key + "): " + popup_buttons.get(key)); + Log.d(TAG, "this: " + this); + Log.d(TAG, "popup_buttons: " + popup_buttons); + } + return popup_buttons.get(key); + } +} diff --git a/src/main/res/drawable-hdpi/flash_auto.png b/src/main/res/drawable-hdpi/flash_auto.png new file mode 100644 index 00000000..cdbac671 Binary files /dev/null and b/src/main/res/drawable-hdpi/flash_auto.png differ diff --git a/src/main/res/drawable-hdpi/flash_off.png b/src/main/res/drawable-hdpi/flash_off.png new file mode 100644 index 00000000..5ff2d9b4 Binary files /dev/null and b/src/main/res/drawable-hdpi/flash_off.png differ diff --git a/src/main/res/drawable-hdpi/flash_on.png b/src/main/res/drawable-hdpi/flash_on.png new file mode 100644 index 00000000..6f629b64 Binary files /dev/null and b/src/main/res/drawable-hdpi/flash_on.png differ diff --git a/src/main/res/drawable-hdpi/ic_burst_mode_white_48dp.png b/src/main/res/drawable-hdpi/ic_burst_mode_white_48dp.png new file mode 100644 index 00000000..b1b848ad Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_burst_mode_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_exposure_white_48dp.png b/src/main/res/drawable-hdpi/ic_exposure_white_48dp.png new file mode 100644 index 00000000..711d8b1e Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_exposure_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_face_white_48dp.png b/src/main/res/drawable-hdpi/ic_face_white_48dp.png new file mode 100644 index 00000000..7e329149 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_face_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_help_outline_white_48dp.png b/src/main/res/drawable-hdpi/ic_help_outline_white_48dp.png new file mode 100644 index 00000000..e9585a04 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_help_outline_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png b/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png new file mode 100644 index 00000000..c41a5fcf Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_launcher_take_photo.png b/src/main/res/drawable-hdpi/ic_launcher_take_photo.png new file mode 100644 index 00000000..63d0b40f Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_launcher_take_photo.png differ diff --git a/src/main/res/drawable-hdpi/ic_mic_red_48dp.png b/src/main/res/drawable-hdpi/ic_mic_red_48dp.png new file mode 100644 index 00000000..2ebca400 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_mic_red_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_mic_white_48dp.png b/src/main/res/drawable-hdpi/ic_mic_white_48dp.png new file mode 100644 index 00000000..b0389382 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_mic_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_more_horiz_white_48dp.png b/src/main/res/drawable-hdpi/ic_more_horiz_white_48dp.png new file mode 100644 index 00000000..2036a9c2 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_more_horiz_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_pause_circle_outline_white_48dp.png b/src/main/res/drawable-hdpi/ic_pause_circle_outline_white_48dp.png new file mode 100644 index 00000000..9be6ac15 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_pause_circle_outline_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_photo_camera_white_48dp.png b/src/main/res/drawable-hdpi/ic_photo_camera_white_48dp.png new file mode 100644 index 00000000..c8e69dce Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_photo_camera_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_photo_size_select_large_white_48dp.png b/src/main/res/drawable-hdpi/ic_photo_size_select_large_white_48dp.png new file mode 100644 index 00000000..2f6ca2c7 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_photo_size_select_large_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_power_settings_new_white_48dp.png b/src/main/res/drawable-hdpi/ic_power_settings_new_white_48dp.png new file mode 100644 index 00000000..95a2b7e7 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_power_settings_new_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_save_white_48dp.png b/src/main/res/drawable-hdpi/ic_save_white_48dp.png new file mode 100644 index 00000000..3e0ce1a5 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_save_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_timer_white_48dp.png b/src/main/res/drawable-hdpi/ic_timer_white_48dp.png new file mode 100644 index 00000000..bb6f9a63 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_timer_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_touch_app_white_48dp.png b/src/main/res/drawable-hdpi/ic_touch_app_white_48dp.png new file mode 100644 index 00000000..4e338c0b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_touch_app_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_videocam_white_48dp.png b/src/main/res/drawable-hdpi/ic_videocam_white_48dp.png new file mode 100644 index 00000000..44c28e2f Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_videocam_white_48dp.png differ diff --git a/src/main/res/drawable-hdpi/settings.png b/src/main/res/drawable-hdpi/settings.png new file mode 100644 index 00000000..54eecded Binary files /dev/null and b/src/main/res/drawable-hdpi/settings.png differ diff --git a/src/main/res/drawable-hdpi/share.png b/src/main/res/drawable-hdpi/share.png new file mode 100644 index 00000000..a36464b8 Binary files /dev/null and b/src/main/res/drawable-hdpi/share.png differ diff --git a/src/main/res/drawable-hdpi/switch_camera.png b/src/main/res/drawable-hdpi/switch_camera.png new file mode 100644 index 00000000..a54c0de3 Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_camera.png differ diff --git a/src/main/res/drawable-hdpi/trash.png b/src/main/res/drawable-hdpi/trash.png new file mode 100644 index 00000000..703b31f8 Binary files /dev/null and b/src/main/res/drawable-hdpi/trash.png differ diff --git a/src/main/res/drawable-mdpi/earth.png b/src/main/res/drawable-mdpi/earth.png new file mode 100644 index 00000000..23fe2608 Binary files /dev/null and b/src/main/res/drawable-mdpi/earth.png differ diff --git a/src/main/res/drawable-mdpi/earth_off.png b/src/main/res/drawable-mdpi/earth_off.png new file mode 100644 index 00000000..85f1760d Binary files /dev/null and b/src/main/res/drawable-mdpi/earth_off.png differ diff --git a/src/main/res/drawable-mdpi/exposure_locked.png b/src/main/res/drawable-mdpi/exposure_locked.png new file mode 100644 index 00000000..083d0482 Binary files /dev/null and b/src/main/res/drawable-mdpi/exposure_locked.png differ diff --git a/src/main/res/drawable-mdpi/exposure_unlocked.png b/src/main/res/drawable-mdpi/exposure_unlocked.png new file mode 100644 index 00000000..cfefff2d Binary files /dev/null and b/src/main/res/drawable-mdpi/exposure_unlocked.png differ diff --git a/src/main/res/drawable-mdpi/flash_auto.png b/src/main/res/drawable-mdpi/flash_auto.png new file mode 100644 index 00000000..9d8d2365 Binary files /dev/null and b/src/main/res/drawable-mdpi/flash_auto.png differ diff --git a/src/main/res/drawable-mdpi/flash_off.png b/src/main/res/drawable-mdpi/flash_off.png new file mode 100644 index 00000000..47696a5b Binary files /dev/null and b/src/main/res/drawable-mdpi/flash_off.png differ diff --git a/src/main/res/drawable-mdpi/flash_on.png b/src/main/res/drawable-mdpi/flash_on.png new file mode 100644 index 00000000..12133e28 Binary files /dev/null and b/src/main/res/drawable-mdpi/flash_on.png differ diff --git a/src/main/res/drawable-mdpi/flash_red_eye.png b/src/main/res/drawable-mdpi/flash_red_eye.png new file mode 100644 index 00000000..b0b18f4c Binary files /dev/null and b/src/main/res/drawable-mdpi/flash_red_eye.png differ diff --git a/src/main/res/drawable-mdpi/flash_torch.png b/src/main/res/drawable-mdpi/flash_torch.png new file mode 100644 index 00000000..ea988261 Binary files /dev/null and b/src/main/res/drawable-mdpi/flash_torch.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_auto.png b/src/main/res/drawable-mdpi/focus_mode_auto.png new file mode 100644 index 00000000..8bd70e78 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_auto.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_continuous_picture.png b/src/main/res/drawable-mdpi/focus_mode_continuous_picture.png new file mode 100644 index 00000000..f4fae8a0 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_continuous_picture.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_continuous_video.png b/src/main/res/drawable-mdpi/focus_mode_continuous_video.png new file mode 100644 index 00000000..7fe0e0e1 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_continuous_video.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_edof.png b/src/main/res/drawable-mdpi/focus_mode_edof.png new file mode 100644 index 00000000..f02b3880 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_edof.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_fixed.png b/src/main/res/drawable-mdpi/focus_mode_fixed.png new file mode 100644 index 00000000..90102a2a Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_fixed.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_infinity.png b/src/main/res/drawable-mdpi/focus_mode_infinity.png new file mode 100644 index 00000000..31c9b0a2 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_infinity.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_locked.png b/src/main/res/drawable-mdpi/focus_mode_locked.png new file mode 100644 index 00000000..90245347 Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_locked.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_macro.png b/src/main/res/drawable-mdpi/focus_mode_macro.png new file mode 100644 index 00000000..2640161e Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_macro.png differ diff --git a/src/main/res/drawable-mdpi/focus_mode_manual.png b/src/main/res/drawable-mdpi/focus_mode_manual.png new file mode 100644 index 00000000..b7e5fbba Binary files /dev/null and b/src/main/res/drawable-mdpi/focus_mode_manual.png differ diff --git a/src/main/res/drawable-mdpi/gallery.png b/src/main/res/drawable-mdpi/gallery.png new file mode 100644 index 00000000..48d982a7 Binary files /dev/null and b/src/main/res/drawable-mdpi/gallery.png differ diff --git a/src/main/res/drawable-mdpi/ic_burst_mode_white_48dp.png b/src/main/res/drawable-mdpi/ic_burst_mode_white_48dp.png new file mode 100644 index 00000000..e2932989 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_burst_mode_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_exposure_white_48dp.png b/src/main/res/drawable-mdpi/ic_exposure_white_48dp.png new file mode 100644 index 00000000..f6f789a8 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_exposure_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_face_white_48dp.png b/src/main/res/drawable-mdpi/ic_face_white_48dp.png new file mode 100644 index 00000000..430be5e8 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_face_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_help_outline_white_48dp.png b/src/main/res/drawable-mdpi/ic_help_outline_white_48dp.png new file mode 100644 index 00000000..e10918b8 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_help_outline_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png b/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png new file mode 100644 index 00000000..c571b2e3 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_launcher_take_photo.png b/src/main/res/drawable-mdpi/ic_launcher_take_photo.png new file mode 100644 index 00000000..cd2ada19 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_launcher_take_photo.png differ diff --git a/src/main/res/drawable-mdpi/ic_mic_red_48dp.png b/src/main/res/drawable-mdpi/ic_mic_red_48dp.png new file mode 100644 index 00000000..984b5095 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_mic_red_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_mic_white_48dp.png b/src/main/res/drawable-mdpi/ic_mic_white_48dp.png new file mode 100644 index 00000000..9f44db5d Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_mic_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_more_horiz_white_48dp.png b/src/main/res/drawable-mdpi/ic_more_horiz_white_48dp.png new file mode 100644 index 00000000..dbb87ca9 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_more_horiz_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_pause_circle_outline_white_48dp.png b/src/main/res/drawable-mdpi/ic_pause_circle_outline_white_48dp.png new file mode 100644 index 00000000..92e6431f Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_pause_circle_outline_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_photo_camera_white_48dp.png b/src/main/res/drawable-mdpi/ic_photo_camera_white_48dp.png new file mode 100644 index 00000000..be9fb226 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_photo_camera_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_photo_size_select_large_white_48dp.png b/src/main/res/drawable-mdpi/ic_photo_size_select_large_white_48dp.png new file mode 100644 index 00000000..4577f75b Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_photo_size_select_large_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_power_settings_new_white_48dp.png b/src/main/res/drawable-mdpi/ic_power_settings_new_white_48dp.png new file mode 100644 index 00000000..6ea9162a Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_power_settings_new_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_save_white_48dp.png b/src/main/res/drawable-mdpi/ic_save_white_48dp.png new file mode 100644 index 00000000..adda0957 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_save_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_timer_white_48dp.png b/src/main/res/drawable-mdpi/ic_timer_white_48dp.png new file mode 100644 index 00000000..9d239966 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_timer_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_touch_app_white_48dp.png b/src/main/res/drawable-mdpi/ic_touch_app_white_48dp.png new file mode 100644 index 00000000..b4f66aee Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_touch_app_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_videocam_white_48dp.png b/src/main/res/drawable-mdpi/ic_videocam_white_48dp.png new file mode 100644 index 00000000..1b2583d3 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_videocam_white_48dp.png differ diff --git a/src/main/res/drawable-mdpi/popup.png b/src/main/res/drawable-mdpi/popup.png new file mode 100644 index 00000000..2f2cb3d0 Binary files /dev/null and b/src/main/res/drawable-mdpi/popup.png differ diff --git a/src/main/res/drawable-mdpi/popup_flash_auto.png b/src/main/res/drawable-mdpi/popup_flash_auto.png new file mode 100644 index 00000000..bd6c51f9 Binary files /dev/null and b/src/main/res/drawable-mdpi/popup_flash_auto.png differ diff --git a/src/main/res/drawable-mdpi/popup_flash_off.png b/src/main/res/drawable-mdpi/popup_flash_off.png new file mode 100644 index 00000000..9b82240e Binary files /dev/null and b/src/main/res/drawable-mdpi/popup_flash_off.png differ diff --git a/src/main/res/drawable-mdpi/popup_flash_on.png b/src/main/res/drawable-mdpi/popup_flash_on.png new file mode 100644 index 00000000..f779e738 Binary files /dev/null and b/src/main/res/drawable-mdpi/popup_flash_on.png differ diff --git a/src/main/res/drawable-mdpi/popup_flash_red_eye.png b/src/main/res/drawable-mdpi/popup_flash_red_eye.png new file mode 100644 index 00000000..df73d4f5 Binary files /dev/null and b/src/main/res/drawable-mdpi/popup_flash_red_eye.png differ diff --git a/src/main/res/drawable-mdpi/popup_flash_torch.png b/src/main/res/drawable-mdpi/popup_flash_torch.png new file mode 100644 index 00000000..40f0b32a Binary files /dev/null and b/src/main/res/drawable-mdpi/popup_flash_torch.png differ diff --git a/src/main/res/drawable-mdpi/settings.png b/src/main/res/drawable-mdpi/settings.png new file mode 100644 index 00000000..25c36db4 Binary files /dev/null and b/src/main/res/drawable-mdpi/settings.png differ diff --git a/src/main/res/drawable-mdpi/share.png b/src/main/res/drawable-mdpi/share.png new file mode 100644 index 00000000..7362f0d7 Binary files /dev/null and b/src/main/res/drawable-mdpi/share.png differ diff --git a/src/main/res/drawable-mdpi/switch_camera.png b/src/main/res/drawable-mdpi/switch_camera.png new file mode 100644 index 00000000..acec8535 Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_camera.png differ diff --git a/src/main/res/drawable-mdpi/switch_video.png b/src/main/res/drawable-mdpi/switch_video.png new file mode 100644 index 00000000..a722cd8f Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_video.png differ diff --git a/src/main/res/drawable-mdpi/take_photo.png b/src/main/res/drawable-mdpi/take_photo.png new file mode 100644 index 00000000..5303b5a2 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_photo.png differ diff --git a/src/main/res/drawable-mdpi/take_photo_pref.png b/src/main/res/drawable-mdpi/take_photo_pref.png new file mode 100644 index 00000000..d04ca572 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_photo_pref.png differ diff --git a/src/main/res/drawable-mdpi/take_photo_pressed.png b/src/main/res/drawable-mdpi/take_photo_pressed.png new file mode 100644 index 00000000..df32cd86 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_photo_pressed.png differ diff --git a/src/main/res/drawable-mdpi/take_video.png b/src/main/res/drawable-mdpi/take_video.png new file mode 100644 index 00000000..f8e02393 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_video.png differ diff --git a/src/main/res/drawable-mdpi/take_video_pref.png b/src/main/res/drawable-mdpi/take_video_pref.png new file mode 100644 index 00000000..6122c149 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_video_pref.png differ diff --git a/src/main/res/drawable-mdpi/take_video_pressed.png b/src/main/res/drawable-mdpi/take_video_pressed.png new file mode 100644 index 00000000..fce04e31 Binary files /dev/null and b/src/main/res/drawable-mdpi/take_video_pressed.png differ diff --git a/src/main/res/drawable-mdpi/take_video_recording.png b/src/main/res/drawable-mdpi/take_video_recording.png new file mode 100644 index 00000000..f6db777e Binary files /dev/null and b/src/main/res/drawable-mdpi/take_video_recording.png differ diff --git a/src/main/res/drawable-mdpi/trash.png b/src/main/res/drawable-mdpi/trash.png new file mode 100644 index 00000000..248fb09c Binary files /dev/null and b/src/main/res/drawable-mdpi/trash.png differ diff --git a/src/main/res/drawable-xhdpi/flash_auto.png b/src/main/res/drawable-xhdpi/flash_auto.png new file mode 100644 index 00000000..9f945bd3 Binary files /dev/null and b/src/main/res/drawable-xhdpi/flash_auto.png differ diff --git a/src/main/res/drawable-xhdpi/flash_off.png b/src/main/res/drawable-xhdpi/flash_off.png new file mode 100644 index 00000000..51eee96c Binary files /dev/null and b/src/main/res/drawable-xhdpi/flash_off.png differ diff --git a/src/main/res/drawable-xhdpi/flash_on.png b/src/main/res/drawable-xhdpi/flash_on.png new file mode 100644 index 00000000..ea11a68b Binary files /dev/null and b/src/main/res/drawable-xhdpi/flash_on.png differ diff --git a/src/main/res/drawable-xhdpi/ic_burst_mode_white_48dp.png b/src/main/res/drawable-xhdpi/ic_burst_mode_white_48dp.png new file mode 100644 index 00000000..a7177b81 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_burst_mode_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_exposure_white_48dp.png b/src/main/res/drawable-xhdpi/ic_exposure_white_48dp.png new file mode 100644 index 00000000..66014de1 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_exposure_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_face_white_48dp.png b/src/main/res/drawable-xhdpi/ic_face_white_48dp.png new file mode 100644 index 00000000..393047f5 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_face_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_help_outline_white_48dp.png b/src/main/res/drawable-xhdpi/ic_help_outline_white_48dp.png new file mode 100644 index 00000000..0f88a236 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_help_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png b/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 00000000..3a82cab3 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_launcher_take_photo.png b/src/main/res/drawable-xhdpi/ic_launcher_take_photo.png new file mode 100644 index 00000000..5e1c365a Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_launcher_take_photo.png differ diff --git a/src/main/res/drawable-xhdpi/ic_mic_red_48dp.png b/src/main/res/drawable-xhdpi/ic_mic_red_48dp.png new file mode 100644 index 00000000..67db462f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_mic_red_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png b/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png new file mode 100644 index 00000000..2f1e60c5 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_more_horiz_white_48dp.png b/src/main/res/drawable-xhdpi/ic_more_horiz_white_48dp.png new file mode 100644 index 00000000..535f0874 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_more_horiz_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_pause_circle_outline_white_48dp.png b/src/main/res/drawable-xhdpi/ic_pause_circle_outline_white_48dp.png new file mode 100644 index 00000000..92b26908 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_pause_circle_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_photo_camera_white_48dp.png b/src/main/res/drawable-xhdpi/ic_photo_camera_white_48dp.png new file mode 100644 index 00000000..777658e9 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_photo_camera_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_photo_size_select_large_white_48dp.png b/src/main/res/drawable-xhdpi/ic_photo_size_select_large_white_48dp.png new file mode 100644 index 00000000..f6ba9d07 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_photo_size_select_large_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_power_settings_new_white_48dp.png b/src/main/res/drawable-xhdpi/ic_power_settings_new_white_48dp.png new file mode 100644 index 00000000..191836bc Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_power_settings_new_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_save_white_48dp.png b/src/main/res/drawable-xhdpi/ic_save_white_48dp.png new file mode 100644 index 00000000..bd80bf1f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_save_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_timer_white_48dp.png b/src/main/res/drawable-xhdpi/ic_timer_white_48dp.png new file mode 100644 index 00000000..b8914c4a Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_timer_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_touch_app_white_48dp.png b/src/main/res/drawable-xhdpi/ic_touch_app_white_48dp.png new file mode 100644 index 00000000..3678975a Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_touch_app_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_videocam_white_48dp.png b/src/main/res/drawable-xhdpi/ic_videocam_white_48dp.png new file mode 100644 index 00000000..ed20c070 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_videocam_white_48dp.png differ diff --git a/src/main/res/drawable-xhdpi/settings.png b/src/main/res/drawable-xhdpi/settings.png new file mode 100644 index 00000000..425a8bc8 Binary files /dev/null and b/src/main/res/drawable-xhdpi/settings.png differ diff --git a/src/main/res/drawable-xhdpi/share.png b/src/main/res/drawable-xhdpi/share.png new file mode 100644 index 00000000..40771e48 Binary files /dev/null and b/src/main/res/drawable-xhdpi/share.png differ diff --git a/src/main/res/drawable-xhdpi/switch_camera.png b/src/main/res/drawable-xhdpi/switch_camera.png new file mode 100644 index 00000000..33ab06e7 Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_camera.png differ diff --git a/src/main/res/drawable-xhdpi/take_photo.png b/src/main/res/drawable-xhdpi/take_photo.png new file mode 100644 index 00000000..7be994e9 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_photo.png differ diff --git a/src/main/res/drawable-xhdpi/take_photo_pref.png b/src/main/res/drawable-xhdpi/take_photo_pref.png new file mode 100644 index 00000000..84828525 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_photo_pref.png differ diff --git a/src/main/res/drawable-xhdpi/take_photo_pressed.png b/src/main/res/drawable-xhdpi/take_photo_pressed.png new file mode 100644 index 00000000..ba121ba3 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_photo_pressed.png differ diff --git a/src/main/res/drawable-xhdpi/take_video.png b/src/main/res/drawable-xhdpi/take_video.png new file mode 100644 index 00000000..a488bd95 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_video.png differ diff --git a/src/main/res/drawable-xhdpi/take_video_pref.png b/src/main/res/drawable-xhdpi/take_video_pref.png new file mode 100644 index 00000000..7383c25f Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_video_pref.png differ diff --git a/src/main/res/drawable-xhdpi/take_video_pressed.png b/src/main/res/drawable-xhdpi/take_video_pressed.png new file mode 100644 index 00000000..dc4c28a5 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_video_pressed.png differ diff --git a/src/main/res/drawable-xhdpi/take_video_recording.png b/src/main/res/drawable-xhdpi/take_video_recording.png new file mode 100644 index 00000000..a4682806 Binary files /dev/null and b/src/main/res/drawable-xhdpi/take_video_recording.png differ diff --git a/src/main/res/drawable-xhdpi/trash.png b/src/main/res/drawable-xhdpi/trash.png new file mode 100644 index 00000000..9eeeed12 Binary files /dev/null and b/src/main/res/drawable-xhdpi/trash.png differ diff --git a/src/main/res/drawable-xxhdpi/earth.png b/src/main/res/drawable-xxhdpi/earth.png new file mode 100644 index 00000000..e0952879 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/earth.png differ diff --git a/src/main/res/drawable-xxhdpi/earth_off.png b/src/main/res/drawable-xxhdpi/earth_off.png new file mode 100644 index 00000000..ee9579d0 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/earth_off.png differ diff --git a/src/main/res/drawable-xxhdpi/flash_auto.png b/src/main/res/drawable-xxhdpi/flash_auto.png new file mode 100644 index 00000000..f4b331bc Binary files /dev/null and b/src/main/res/drawable-xxhdpi/flash_auto.png differ diff --git a/src/main/res/drawable-xxhdpi/flash_off.png b/src/main/res/drawable-xxhdpi/flash_off.png new file mode 100644 index 00000000..534f547c Binary files /dev/null and b/src/main/res/drawable-xxhdpi/flash_off.png differ diff --git a/src/main/res/drawable-xxhdpi/flash_on.png b/src/main/res/drawable-xxhdpi/flash_on.png new file mode 100644 index 00000000..3d75e9ea Binary files /dev/null and b/src/main/res/drawable-xxhdpi/flash_on.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_burst_mode_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_burst_mode_white_48dp.png new file mode 100644 index 00000000..bf58c0aa Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_burst_mode_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_exposure_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_exposure_white_48dp.png new file mode 100644 index 00000000..29656a27 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_exposure_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_face_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_face_white_48dp.png new file mode 100644 index 00000000..3b4ea3c6 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_face_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_help_outline_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_help_outline_white_48dp.png new file mode 100644 index 00000000..c47c09c0 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_help_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 00000000..bc0eda9f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_launcher_take_photo.png b/src/main/res/drawable-xxhdpi/ic_launcher_take_photo.png new file mode 100644 index 00000000..69feda86 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_launcher_take_photo.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_mic_red_48dp.png b/src/main/res/drawable-xxhdpi/ic_mic_red_48dp.png new file mode 100644 index 00000000..efc92a2d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_mic_red_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png new file mode 100644 index 00000000..ad0460c0 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_more_horiz_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_more_horiz_white_48dp.png new file mode 100644 index 00000000..902df1e8 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_more_horiz_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_pause_circle_outline_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_pause_circle_outline_white_48dp.png new file mode 100644 index 00000000..cf27e052 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_pause_circle_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_photo_camera_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_photo_camera_white_48dp.png new file mode 100644 index 00000000..a4e7aea7 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_photo_camera_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_photo_size_select_large_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_photo_size_select_large_white_48dp.png new file mode 100644 index 00000000..fd01c03f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_photo_size_select_large_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_power_settings_new_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_power_settings_new_white_48dp.png new file mode 100644 index 00000000..0b95e724 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_power_settings_new_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_save_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_save_white_48dp.png new file mode 100644 index 00000000..3b9de2bf Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_save_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_timer_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_timer_white_48dp.png new file mode 100644 index 00000000..7f1b541d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_timer_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_touch_app_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_touch_app_white_48dp.png new file mode 100644 index 00000000..ad0a2f7a Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_touch_app_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_videocam_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_videocam_white_48dp.png new file mode 100644 index 00000000..eff5923d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_videocam_white_48dp.png differ diff --git a/src/main/res/drawable-xxhdpi/settings.png b/src/main/res/drawable-xxhdpi/settings.png new file mode 100644 index 00000000..fe5fec47 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/settings.png differ diff --git a/src/main/res/drawable-xxhdpi/share.png b/src/main/res/drawable-xxhdpi/share.png new file mode 100644 index 00000000..22ed428f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/share.png differ diff --git a/src/main/res/drawable-xxhdpi/switch_camera.png b/src/main/res/drawable-xxhdpi/switch_camera.png new file mode 100644 index 00000000..acc42662 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_camera.png differ diff --git a/src/main/res/drawable-xxhdpi/trash.png b/src/main/res/drawable-xxhdpi/trash.png new file mode 100644 index 00000000..cb1260a4 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/trash.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_burst_mode_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_burst_mode_white_48dp.png new file mode 100644 index 00000000..141ce4a3 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_burst_mode_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_exposure_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_exposure_white_48dp.png new file mode 100644 index 00000000..dde28e08 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_exposure_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_face_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_face_white_48dp.png new file mode 100644 index 00000000..6d124349 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_face_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_help_outline_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_help_outline_white_48dp.png new file mode 100644 index 00000000..3b604fc7 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_help_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 00000000..939ee3a9 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_more_horiz_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_more_horiz_white_48dp.png new file mode 100644 index 00000000..00c03684 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_more_horiz_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_pause_circle_outline_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_pause_circle_outline_white_48dp.png new file mode 100644 index 00000000..2693f256 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_pause_circle_outline_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_48dp.png new file mode 100644 index 00000000..f2fe54bd Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_photo_size_select_large_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_photo_size_select_large_white_48dp.png new file mode 100644 index 00000000..fb0a6b6a Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_photo_size_select_large_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_power_settings_new_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_power_settings_new_white_48dp.png new file mode 100644 index 00000000..4e42d065 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_power_settings_new_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_save_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_save_white_48dp.png new file mode 100644 index 00000000..4243804c Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_save_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_timer_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_timer_white_48dp.png new file mode 100644 index 00000000..e9b65514 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_timer_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_touch_app_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_touch_app_white_48dp.png new file mode 100644 index 00000000..77312464 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_touch_app_white_48dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_videocam_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_videocam_white_48dp.png new file mode 100644 index 00000000..c384d594 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_videocam_white_48dp.png differ diff --git a/src/main/res/drawable/take_photo_selector.xml b/src/main/res/drawable/take_photo_selector.xml new file mode 100644 index 00000000..d6867e2b --- /dev/null +++ b/src/main/res/drawable/take_photo_selector.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/res/drawable/take_video_selector.xml b/src/main/res/drawable/take_video_selector.xml new file mode 100644 index 00000000..3aa5e206 --- /dev/null +++ b/src/main/res/drawable/take_video_selector.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..1e47d72e --- /dev/null +++ b/src/main/res/layout/activity_main.xml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + +